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 8ff93ba04b87caa67aa203c952ec3f2797bcae3f Author: ddekany <[email protected]> AuthorDate: Tue Dec 31 01:08:41 2019 +0100 Added WhitelistMemberAccessPolicy and related internal classes --- .../java/freemarker/ext/beans/BeansWrapper.java | 25 +- .../freemarker/ext/beans/ClassIntrospector.java | 79 +-- .../ext/beans/ClassIntrospectorBuilder.java | 6 + ...erAccessPolicy.java => ConstructorMatcher.java} | 18 +- .../ext/beans/ExecutableMemberSignature.java | 69 +++ .../{MemberAccessPolicy.java => FieldMatcher.java} | 18 +- .../freemarker/ext/beans/MemberAccessPolicy.java | 39 +- .../java/freemarker/ext/beans/MemberMatcher.java | 111 ++++ ...{MemberAccessPolicy.java => MethodMatcher.java} | 22 +- .../freemarker/ext/beans/TemplateAccessible.java | 45 ++ .../ext/beans/WhitelistMemberAccessPolicy.java | 411 +++++++++++++++ .../java/freemarker/ext/beans/_MethodUtil.java | 141 ++++++ .../freemarker/template/DefaultObjectWrapper.java | 10 +- .../freemarker/template/utility/ClassUtil.java | 37 +- src/manual/en_US/book.xml | 74 ++- ...DefaultObjectWrapperMemberAccessPolicyTest.java | 115 ++++- .../freemarker/ext/beans/MethodMatcherTest.java | 179 +++++++ .../java/freemarker/ext/beans/MethodUtilTest.java | 156 ++++++ .../ext/beans/WhitelistMemberAccessPolicyTest.java | 558 +++++++++++++++++++++ .../template/DefaultObjectWrapperTest.java | 35 ++ 20 files changed, 2031 insertions(+), 117 deletions(-) diff --git a/src/main/java/freemarker/ext/beans/BeansWrapper.java b/src/main/java/freemarker/ext/beans/BeansWrapper.java index d014a69..d67c3ac 100644 --- a/src/main/java/freemarker/ext/beans/BeansWrapper.java +++ b/src/main/java/freemarker/ext/beans/BeansWrapper.java @@ -658,6 +658,29 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { } } + /** + * @since 2.3.30 + */ + public MemberAccessPolicy getMemberAccessPolicy() { + return classIntrospector.getMemberAccessPolicy(); + } + + /** + * Used to customize what members will be hidden; + * see {@link BeansWrapperBuilder#setMemberAccessPolicy(MemberAccessPolicy)} for more. + * + * @since 2.3.30 + */ + public void setMemberAccessPolicy(MemberAccessPolicy memberAccessPolicy) { + checkModifiable(); + + if (classIntrospector.getMemberAccessPolicy() != memberAccessPolicy) { + ClassIntrospectorBuilder builder = classIntrospector.createBuilder(); + builder.setMemberAccessPolicy(memberAccessPolicy); + replaceClassIntrospector(builder); + } + } + MethodSorter getMethodSorter() { return classIntrospector.getMethodSorter(); } @@ -1567,7 +1590,7 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { Object ctors = classIntrospector.get(clazz).get(ClassIntrospector.CONSTRUCTORS_KEY); if (ctors == null) { throw new TemplateModelException("Class " + clazz.getName() + - " has no public constructors."); + " has no exposed constructors."); } Constructor<?> ctor = null; Object[] objargs; diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospector.java b/src/main/java/freemarker/ext/beans/ClassIntrospector.java index 72f26cb..630bf95 100644 --- a/src/main/java/freemarker/ext/beans/ClassIntrospector.java +++ b/src/main/java/freemarker/ext/beans/ClassIntrospector.java @@ -78,6 +78,11 @@ class ClassIntrospector { private static final String JREBEL_INTEGRATION_ERROR_MSG = "Error initializing JRebel integration. JRebel integration disabled."; + private static final ExecutableMemberSignature GET_STRING_SIGNATURE = + new ExecutableMemberSignature("get", new Class[] { String.class }); + private static final ExecutableMemberSignature GET_OBJECT_SIGNATURE = + new ExecutableMemberSignature("get", new Class[] { Object.class }); + /** * When this property is true, some things are stricter. This is mostly to catch suspicious things in development * that can otherwise be valid situations. @@ -205,7 +210,7 @@ class ClassIntrospector { return new ClassIntrospectorBuilder(this); } - // ------------------------------------------------------------------------------------------------------------------ + // ----------------------------------------------------------------------------------------------------------------- // Introspection: /** @@ -273,7 +278,7 @@ class ClassIntrospector { addFieldsToClassIntrospectionData(introspData, clazz, classMemberAccessPolicy); } - final Map<MethodSignature, List<Method>> accessibleMethods = discoverAccessibleMethods(clazz); + final Map<ExecutableMemberSignature, List<Method>> accessibleMethods = discoverAccessibleMethods(clazz); addGenericGetToClassIntrospectionData(introspData, accessibleMethods, classMemberAccessPolicy); @@ -312,7 +317,8 @@ class ClassIntrospector { } private void addBeanInfoToClassIntrospectionData( - Map<Object, Object> introspData, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods, + Map<Object, Object> introspData, Class<?> clazz, + Map<ExecutableMemberSignature, List<Method>> accessibleMethods, ClassMemberAccessPolicy classMemberAccessPolicy) throws IntrospectionException { BeanInfo beanInfo = Introspector.getBeanInfo(clazz); List<PropertyDescriptor> pdas = getPropertyDescriptors(beanInfo, clazz); @@ -660,7 +666,8 @@ class ClassIntrospector { private void addPropertyDescriptorToClassIntrospectionData(Map<Object, Object> introspData, PropertyDescriptor pd, - Map<MethodSignature, List<Method>> accessibleMethods, ClassMemberAccessPolicy classMemberAccessPolicy) { + Map<ExecutableMemberSignature, List<Method>> accessibleMethods, + ClassMemberAccessPolicy classMemberAccessPolicy) { Method readMethod = getMatchingAccessibleMethod(pd.getReadMethod(), accessibleMethods); if (readMethod != null && !isMethodExposed(classMemberAccessPolicy, readMethod)) { readMethod = null; @@ -687,12 +694,11 @@ class ClassIntrospector { } private void addGenericGetToClassIntrospectionData(Map<Object, Object> introspData, - Map<MethodSignature, List<Method>> accessibleMethods, ClassMemberAccessPolicy classMemberAccessPolicy) { - Method genericGet = getFirstAccessibleMethod( - MethodSignature.GET_STRING_SIGNATURE, accessibleMethods); + Map<ExecutableMemberSignature, List<Method>> accessibleMethods, + ClassMemberAccessPolicy classMemberAccessPolicy) { + Method genericGet = getFirstAccessibleMethod(GET_STRING_SIGNATURE, accessibleMethods); if (genericGet == null) { - genericGet = getFirstAccessibleMethod( - MethodSignature.GET_OBJECT_SIGNATURE, accessibleMethods); + genericGet = getFirstAccessibleMethod(GET_OBJECT_SIGNATURE, accessibleMethods); } if (genericGet != null && isMethodExposed(classMemberAccessPolicy, genericGet)) { introspData.put(GENERIC_GET_KEY, genericGet); @@ -730,23 +736,24 @@ class ClassIntrospector { } /** - * Retrieves mapping of {@link MethodSignature}-s to a {@link List} of accessible methods for a class. In case the - * class is not public, retrieves methods with same signature as its public methods from public superclasses and - * interfaces. Basically upcasts every method to the nearest accessible method. + * Retrieves mapping of {@link ExecutableMemberSignature}-s to a {@link List} of accessible methods for a class. In + * case the class is not public, retrieves methods with same signature as its public methods from public + * superclasses and interfaces. Basically upcasts every method to the nearest accessible method. */ - private static Map<MethodSignature, List<Method>> discoverAccessibleMethods(Class<?> clazz) { - Map<MethodSignature, List<Method>> accessibles = new HashMap<MethodSignature, List<Method>>(); + private static Map<ExecutableMemberSignature, List<Method>> discoverAccessibleMethods(Class<?> clazz) { + Map<ExecutableMemberSignature, List<Method>> accessibles = new HashMap<ExecutableMemberSignature, List<Method>>(); discoverAccessibleMethods(clazz, accessibles); return accessibles; } - private static void discoverAccessibleMethods(Class<?> clazz, Map<MethodSignature, List<Method>> accessibles) { + private static void discoverAccessibleMethods( + Class<?> clazz, Map<ExecutableMemberSignature, List<Method>> accessibles) { if (Modifier.isPublic(clazz.getModifiers())) { try { Method[] methods = clazz.getMethods(); for (int i = 0; i < methods.length; i++) { Method method = methods[i]; - MethodSignature sig = new MethodSignature(method); + ExecutableMemberSignature sig = new ExecutableMemberSignature(method); // Contrary to intuition, a class can actually have several // different methods with same signature *but* different // return types. These can't be constructed using Java the @@ -785,11 +792,11 @@ class ClassIntrospector { } } - private static Method getMatchingAccessibleMethod(Method m, Map<MethodSignature, List<Method>> accessibles) { + private static Method getMatchingAccessibleMethod(Method m, Map<ExecutableMemberSignature, List<Method>> accessibles) { if (m == null) { return null; } - MethodSignature sig = new MethodSignature(m); + ExecutableMemberSignature sig = new ExecutableMemberSignature(m); List<Method> ams = accessibles.get(sig); if (ams == null) { return null; @@ -802,7 +809,8 @@ class ClassIntrospector { return null; } - private static Method getFirstAccessibleMethod(MethodSignature sig, Map<MethodSignature, List<Method>> accessibles) { + private static Method getFirstAccessibleMethod( + ExecutableMemberSignature sig, Map<ExecutableMemberSignature, List<Method>> accessibles) { List<Method> ams = accessibles.get(sig); if (ams == null || ams.isEmpty()) { return null; @@ -853,39 +861,6 @@ class ClassIntrospector { return argTypes; } - private static final class MethodSignature { - private static final MethodSignature GET_STRING_SIGNATURE = - new MethodSignature("get", new Class[] { String.class }); - private static final MethodSignature GET_OBJECT_SIGNATURE = - new MethodSignature("get", new Class[] { Object.class }); - - private final String name; - private final Class<?>[] args; - - private MethodSignature(String name, Class<?>[] args) { - this.name = name; - this.args = args; - } - - MethodSignature(Method method) { - this(method.getName(), method.getParameterTypes()); - } - - @Override - public boolean equals(Object o) { - if (o instanceof MethodSignature) { - MethodSignature ms = (MethodSignature) o; - return ms.name.equals(name) && Arrays.equals(args, ms.args); - } - return false; - } - - @Override - public int hashCode() { - return name.hashCode() ^ args.length; // TODO That's a poor quality hash... isn't this a problem? - } - } - // ----------------------------------------------------------------------------------------------------------------- // Cache management: diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java b/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java index 1f2d5e0..e2847ab 100644 --- a/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java +++ b/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java @@ -156,6 +156,12 @@ final class ClassIntrospectorBuilder implements Cloneable { return memberAccessPolicy; } + /** + * Sets the {@link MemberAccessPolicy}; default is {@link DefaultMemberAccessPolicy#getInstance(Version)}, which + * is not appropriate if template editors aren't trusted. + * + * @since 2.3.30 + */ public void setMemberAccessPolicy(MemberAccessPolicy memberAccessPolicy) { NullArgumentException.check(memberAccessPolicy); this.memberAccessPolicy = memberAccessPolicy; diff --git a/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/ConstructorMatcher.java similarity index 53% copy from src/main/java/freemarker/ext/beans/MemberAccessPolicy.java copy to src/main/java/freemarker/ext/beans/ConstructorMatcher.java index 5d72fea..2c94576 100644 --- a/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java +++ b/src/main/java/freemarker/ext/beans/ConstructorMatcher.java @@ -19,18 +19,16 @@ package freemarker.ext.beans; +import java.lang.reflect.Constructor; + /** - * Implement this to specify what class members are accessible from templates. Implementations must be thread - * safe, and instances should be generally singletons on JVM level. The last is because FreeMarker tries to cache - * class introspectors in a global (static, JVM-scope) cache for reuse, and that's only possible if the - * {@link MemberAccessPolicy} instances used at different places in the JVM are equal according to - * {@link #equals(Object) (and the singleton object of course {@link #equals(Object)} with itself). + * {@link MemberMatcher} for constructors. * * @since 2.3.30 */ -public interface MemberAccessPolicy { - /** - * Returns the {@link ClassMemberAccessPolicy} that encapsulates the member access policy for a given class. - */ - ClassMemberAccessPolicy forClass(Class<?> containingClass); +final class ConstructorMatcher extends MemberMatcher<Constructor<?>, ExecutableMemberSignature> { + @Override + protected ExecutableMemberSignature toMemberSignature(Constructor<?> member) { + return new ExecutableMemberSignature(member); + } } diff --git a/src/main/java/freemarker/ext/beans/ExecutableMemberSignature.java b/src/main/java/freemarker/ext/beans/ExecutableMemberSignature.java new file mode 100644 index 0000000..dfba692 --- /dev/null +++ b/src/main/java/freemarker/ext/beans/ExecutableMemberSignature.java @@ -0,0 +1,69 @@ +/* + * 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.ext.beans; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; + +/** + * Used as a key in a {@link Map} or {@link Set} of methods or constructors. + * + * @since 2.3.30 + */ +final class ExecutableMemberSignature { + private final String name; + private final Class<?>[] args; + + ExecutableMemberSignature(String name, Class<?>[] args) { + this.name = name; + this.args = args; + } + + /** + * Uses the method name, and the parameter types. + */ + ExecutableMemberSignature(Method method) { + this(method.getName(), method.getParameterTypes()); + } + + /** + * Doesn't use the constructor name, only the parameter types. + */ + ExecutableMemberSignature(Constructor<?> constructor) { + this("<init>", constructor.getParameterTypes()); + } + + @Override + public boolean equals(Object o) { + if (o instanceof ExecutableMemberSignature) { + ExecutableMemberSignature ms = (ExecutableMemberSignature) o; + return ms.name.equals(name) && Arrays.equals(args, ms.args); + } + return false; + } + + @Override + public int hashCode() { + return name.hashCode() + args.length * 31; + } +} diff --git a/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/FieldMatcher.java similarity index 53% copy from src/main/java/freemarker/ext/beans/MemberAccessPolicy.java copy to src/main/java/freemarker/ext/beans/FieldMatcher.java index 5d72fea..f67bf14 100644 --- a/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java +++ b/src/main/java/freemarker/ext/beans/FieldMatcher.java @@ -19,18 +19,16 @@ package freemarker.ext.beans; +import java.lang.reflect.Field; + /** - * Implement this to specify what class members are accessible from templates. Implementations must be thread - * safe, and instances should be generally singletons on JVM level. The last is because FreeMarker tries to cache - * class introspectors in a global (static, JVM-scope) cache for reuse, and that's only possible if the - * {@link MemberAccessPolicy} instances used at different places in the JVM are equal according to - * {@link #equals(Object) (and the singleton object of course {@link #equals(Object)} with itself). + * {@link MemberMatcher} for fields. * * @since 2.3.30 */ -public interface MemberAccessPolicy { - /** - * Returns the {@link ClassMemberAccessPolicy} that encapsulates the member access policy for a given class. - */ - ClassMemberAccessPolicy forClass(Class<?> containingClass); +final class FieldMatcher extends MemberMatcher<Field, String> { + @Override + protected String toMemberSignature(Field member) { + return member.getName(); + } } diff --git a/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java index 5d72fea..c8de56d 100644 --- a/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java +++ b/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java @@ -19,18 +19,45 @@ package freemarker.ext.beans; +import freemarker.template.DefaultObjectWrapper; +import freemarker.template.DefaultObjectWrapperBuilder; +import freemarker.template.ObjectWrapper; +import freemarker.template.TemplateModel; + /** - * Implement this to specify what class members are accessible from templates. Implementations must be thread - * safe, and instances should be generally singletons on JVM level. The last is because FreeMarker tries to cache - * class introspectors in a global (static, JVM-scope) cache for reuse, and that's only possible if the - * {@link MemberAccessPolicy} instances used at different places in the JVM are equal according to - * {@link #equals(Object) (and the singleton object of course {@link #equals(Object)} with itself). + * Implement this to specify what class members are accessible from templates. + * + * <p>The instance is usually set via {@link BeansWrapperBuilder#setMemberAccessPolicy(MemberAccessPolicy)} (or if + * you use {@link DefaultObjectWrapper}, with + * {@link DefaultObjectWrapperBuilder#setMemberAccessPolicy(MemberAccessPolicy)}). + * + * <p>As {@link BeansWrapper}, and its subclasses like {@link DefaultObjectWrapper}, only discover public + * members, it's pointless to whitelist non-public members. An {@link MemberAccessPolicy} is a filter applied to + * the set of members that {@link BeansWrapper} intends to expose on the first place. (Also, while public members + * declared in non-public classes are discovered by {@link BeansWrapper}, Java reflection will not allow accessing those + * normally, so generally it's not useful to whitelist those either.) + * + * <p>Note that if you add {@link TemplateModel}-s directly to the data-model, those are not wrapped by the + * {@link ObjectWrapper}, and so the {@link MemberAccessPolicy} won't affect those. + * + * <p>Implementations must be thread-safe, and instances generally should be singletons on JVM level. FreeMarker + * caches its class metadata in a global (static, JVM-scope) cache for shared use, and the {@link MemberAccessPolicy} + * used is part of the cache key. Thus {@link MemberAccessPolicy} instances used at different places in the JVM + * should be equal according to {@link Object#equals(Object)}, as far as they implement exactly the same policy. It's + * not recommended to override {@link Object#equals(Object)}; use singletons and the default + * {@link Object#equals(Object)} implementation if possible. * * @since 2.3.30 */ public interface MemberAccessPolicy { /** * Returns the {@link ClassMemberAccessPolicy} that encapsulates the member access policy for a given class. + * {@link ClassMemberAccessPolicy} implementations need not be thread-safe. Because class introspection results are + * cached, and so this method is usually only called once for a given class, the {@link ClassMemberAccessPolicy} + * instances shouldn't be cached by the implementation of this method. + * + * @param contextClass + * The exact class of object from which members will be get in the templates. */ - ClassMemberAccessPolicy forClass(Class<?> containingClass); + ClassMemberAccessPolicy forClass(Class<?> contextClass); } diff --git a/src/main/java/freemarker/ext/beans/MemberMatcher.java b/src/main/java/freemarker/ext/beans/MemberMatcher.java new file mode 100644 index 0000000..2b52178 --- /dev/null +++ b/src/main/java/freemarker/ext/beans/MemberMatcher.java @@ -0,0 +1,111 @@ +/* + * 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.ext.beans; + +import java.lang.reflect.Member; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * For implementing a whitelist or blacklist of class members in {@link MemberAccessPolicy} implementations. + * A {@link MemberMatcher} filters by name and/or signature, but not by by visibility, as + * the visibility condition is orthogonal to the whitelist or blacklist content. + * + * @since 2.3.30 + */ +abstract class MemberMatcher<M extends Member, S> { + private final Map<S, Types> signaturesToUpperBoundTypes = new HashMap<S, Types>(); + + private static class Types { + private final Set<Class<?>> set = new HashSet<Class<?>>(); + private boolean containsInterfaces; + } + + /** + * Returns the {@link Map} lookup key used to match the member. + */ + protected abstract S toMemberSignature(M member); + + /** + * Adds a member that this {@link MemberMatcher} will match. + * + * @param upperBoundType + * The type of the actual object that contains the member must {@code instanceof} this. + * @param member + * The member that should match (when the upper bound class condition is also fulfilled). Only the name + * and/or signature of the member will be used for the condition, not the actual member object. + */ + void addMatching(Class<?> upperBoundType, M member) { + Class<?> declaringClass = member.getDeclaringClass(); + if (!declaringClass.isAssignableFrom(upperBoundType)) { + throw new IllegalArgumentException("Upper bound class " + upperBoundType.getName() + " is not the same " + + "type or a subtype of the declaring type of member " + member + "."); + } + + S memberSignature = toMemberSignature(member); + Types upperBoundTypes = signaturesToUpperBoundTypes.get(memberSignature); + if (upperBoundTypes == null) { + upperBoundTypes = new Types(); + signaturesToUpperBoundTypes.put(memberSignature, upperBoundTypes); + } + upperBoundTypes.set.add(upperBoundType); + if (upperBoundType.isInterface()) { + upperBoundTypes.containsInterfaces = true; + } + } + + /** + * Returns if the given member, if it's referred through the given class, is matched by this {@link MemberMatcher}. + * + * @param contextClass The actual class through which we access the member + * @param member The member that we intend to access + * + * @return If there was match in this {@link MemberMatcher}. + */ + boolean matches(Class<?> contextClass, M member) { + S memberSignature = toMemberSignature(member); + Types upperBoundTypes = signaturesToUpperBoundTypes.get(memberSignature); + + return upperBoundTypes != null && containsTypeOrSuperType(upperBoundTypes, contextClass); + } + + private static boolean containsTypeOrSuperType(Types types, Class<?> c) { + if (c == null) { + return false; + } + + if (types.set.contains(c)) { + return true; + } + if (containsTypeOrSuperType(types, c.getSuperclass())) { + return true; + } + if (types.containsInterfaces) { + for (Class<?> anInterface : c.getInterfaces()) { + if (containsTypeOrSuperType(types, anInterface)) { + return true; + } + } + } + return false; + } +} diff --git a/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/MethodMatcher.java similarity index 53% copy from src/main/java/freemarker/ext/beans/MemberAccessPolicy.java copy to src/main/java/freemarker/ext/beans/MethodMatcher.java index 5d72fea..7df56be 100644 --- a/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java +++ b/src/main/java/freemarker/ext/beans/MethodMatcher.java @@ -19,18 +19,20 @@ package freemarker.ext.beans; +import java.lang.reflect.Method; + /** - * Implement this to specify what class members are accessible from templates. Implementations must be thread - * safe, and instances should be generally singletons on JVM level. The last is because FreeMarker tries to cache - * class introspectors in a global (static, JVM-scope) cache for reuse, and that's only possible if the - * {@link MemberAccessPolicy} instances used at different places in the JVM are equal according to - * {@link #equals(Object) (and the singleton object of course {@link #equals(Object)} with itself). + * {@link MemberMatcher} for methods. + * + * <p>The return type (and visibility) of the methods will be ignored, only the method name and its parameter types + * matter. (The {@link MemberAccessPolicy}, and even {@link BeansWrapper} itself will still filter by visibility, it's + * just not the duty of the {@link MemberMatcher}.) * * @since 2.3.30 */ -public interface MemberAccessPolicy { - /** - * Returns the {@link ClassMemberAccessPolicy} that encapsulates the member access policy for a given class. - */ - ClassMemberAccessPolicy forClass(Class<?> containingClass); +final class MethodMatcher extends MemberMatcher<Method, ExecutableMemberSignature> { + @Override + protected ExecutableMemberSignature toMemberSignature(Method member) { + return new ExecutableMemberSignature(member); + } } diff --git a/src/main/java/freemarker/ext/beans/TemplateAccessible.java b/src/main/java/freemarker/ext/beans/TemplateAccessible.java new file mode 100644 index 0000000..5e07873 --- /dev/null +++ b/src/main/java/freemarker/ext/beans/TemplateAccessible.java @@ -0,0 +1,45 @@ +/* + * 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.ext.beans; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import freemarker.template.DefaultObjectWrapper; +import freemarker.template.ObjectWrapper; + +/** + * Indicates that the the annotated member can be exposed to templates; if the annotated member will be actually + * exposed depends on the {@link ObjectWrapper} in use, and how that was configured. When used with + * {@link BeansWrapper} or its subclasses, most notably with {@link DefaultObjectWrapper}, and you also set the + * {@link MemberAccessPolicy} to a {@link WhitelistMemberAccessPolicy}, it will acts as if the members annotated with + * this are in the whitelist. Note that adding something to the whitelist doesn't necessary make it visible from + * templates; see {@link WhitelistMemberAccessPolicy} documentation. + * + * @since 2.3.30 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD}) +public @interface TemplateAccessible { +} diff --git a/src/main/java/freemarker/ext/beans/WhitelistMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/WhitelistMemberAccessPolicy.java new file mode 100644 index 0000000..5e8945a --- /dev/null +++ b/src/main/java/freemarker/ext/beans/WhitelistMemberAccessPolicy.java @@ -0,0 +1,411 @@ +/* + * 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.ext.beans; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.StringTokenizer; + +import freemarker.log.Logger; +import freemarker.template.ObjectWrapper; +import freemarker.template.utility.ClassUtil; +import freemarker.template.utility.NullArgumentException; + +/** + * Whitelist-based member access policy, that is, only members that you have explicitly whitelisted will be accessible. + * The whitelist content is application specific, and can be significant work to put together, but it's the only way + * you can achieve any practical safety if you don't fully trust the users who can edit templates. Of course, this only + * can deal with the {@link ObjectWrapper} aspect of safety; please check the Manual to see what else is needed. Also, + * since this is related to security, read the documentation of {@link MemberAccessPolicy}, to know about the + * pitfalls and edge cases related to {@link MemberAccessPolicy}-es in general. + * + * <p>There are two ways you can add members to the whitelist: + * <ul> + * <li>Via a list of member selectors passed to the constructor + * <li>Via {@link TemplateAccessible} annotation + * </ul> + * + * <p>When a member is whitelisted, it's identified by the following data (with the example of + * {@code com.example.MyClass.myMethod(int, int)} being whitelisted): + * <ul> + * <li>Upper bound class ({@code com.example.MyClass} in the example) + * <li>Member name ({@code myMethod} in the example), except for constructors where it's unused + * <li>Parameter types ({@code int, int} in the example), except for fields where it's unused + * </ul> + * + * <p>Once you have whitelisted a member in the upper bound class, it will be automatically whitelisted in all + * subclasses of that, even if the whitelisted member is a field or constructor (which doesn't support overriding, but + * it will be treated as such if the field name or constructor parameter types match). + * It's called "upper bound" class, because the member will only be whitelisted in classes that are {@code instanceof} + * the upper bound class. That restriction stands even if the member was inherited from another class or + * interface, and it wasn't even overridden in the upper bound class; the member won't be whitelisted in the + * class/interface where it was inherited from, if that type is more generic than the upper bound class. + * + * <p>Note that the return type of methods aren't used in any way. So if you whitelist {@code myMethod(int, int)}, and + * it has multiple variants with different return types (which is possible on the bytecode level), then you have + * whitelisted all variants of it. + * + * @since 2.3.30 + */ +public class WhitelistMemberAccessPolicy implements MemberAccessPolicy { + private static final Logger LOG = Logger.getLogger("freemarker.beans"); + + private final MethodMatcher methodMatcher; + private final ConstructorMatcher constructorMatcher; + private final FieldMatcher fieldMatcher; + + /** + * A condition that matches some type members. See {@link WhitelistMemberAccessPolicy} documentation for more. + * Exactly one of these will be non-{@code null}: + * {@link #getMethod()}, {@link #getConstructor()}, {@link #getField()}, {@link #getException()}. + * + * @since 2.3.30 + */ + public final static class MemberSelector { + private final Class<?> upperBoundType; + private final Method method; + private final Constructor<?> constructor; + private final Field field; + private final Exception exception; + private final String exceptionMemberSelectorString; + + /** + * Use if you want to match methods similar to the specified one, in types that are {@code instanceof} of + * the specified upper bound type. When methods are matched, only the name and the parameter types matter. + */ + public MemberSelector(Class<?> upperBoundType, Method method) { + NullArgumentException.check("upperBoundType", upperBoundType); + NullArgumentException.check("method", method); + this.upperBoundType = upperBoundType; + this.method = method; + this.constructor = null; + this.field = null; + this.exception = null; + this.exceptionMemberSelectorString = null; + } + + /** + * Use if you want to match constructors similar to the specified one, in types that are {@code instanceof} of + * the specified upper bound type. When constructors are matched, only the parameter types matter. + */ + public MemberSelector(Class<?> upperBoundType, Constructor<?> constructor) { + NullArgumentException.check("upperBoundType", upperBoundType); + NullArgumentException.check("constructor", constructor); + this.upperBoundType = upperBoundType; + this.method = null; + this.constructor = constructor; + this.field = null; + this.exception = null; + this.exceptionMemberSelectorString = null; + } + + /** + * Use if you want to match fields similar to the specified one, in types that are {@code instanceof} of + * the specified upper bound type. When fields are matched, only the name matters. + */ + public MemberSelector(Class<?> upperBoundType, Field field) { + NullArgumentException.check("upperBoundType", upperBoundType); + NullArgumentException.check("field", field); + this.upperBoundType = upperBoundType; + this.method = null; + this.constructor = null; + this.field = field; + this.exception = null; + this.exceptionMemberSelectorString = null; + } + + /** + * Used to store the result of a parsing that's failed for a reason that we can skip on runtime (typically, + * when a missing class or member was referred). + * + * @param upperBoundType {@code null} if resolving the upper bound type itself failed. + * @param exception Not {@code null} + * @param exceptionMemberSelectorString Not {@code null}; the selector whose resolution has failed, used in + * the log message. + */ + public MemberSelector(Class<?> upperBoundType, Exception exception, String exceptionMemberSelectorString) { + NullArgumentException.check("exception", exception); + NullArgumentException.check("exceptionMemberSelectorString", exceptionMemberSelectorString); + this.upperBoundType = upperBoundType; + this.method = null; + this.constructor = null; + this.field = null; + this.exception = exception; + this.exceptionMemberSelectorString = exceptionMemberSelectorString; + } + + /** + * Maybe {@code null} if {@link #getException()} is non-{@code null}. + */ + public Class<?> getUpperBoundType() { + return upperBoundType; + } + + /** + * Maybe {@code null}; + * set if the selector matches methods similar to the returned one, and there was no exception. + */ + public Method getMethod() { + return method; + } + + /** + * Maybe {@code null}; + * set if the selector matches constructors similar to the returned one, and there was no exception. + */ + public Constructor<?> getConstructor() { + return constructor; + } + + /** + * Maybe {@code null}; + * set if the selector matches fields similar to the returned one, and there was no exception. + */ + public Field getField() { + return field; + } + + /** + * Maybe {@code null} + */ + public Exception getException() { + return exception; + } + + /** + * Maybe {@code null} + */ + public String getExceptionMemberSelectorString() { + return exceptionMemberSelectorString; + } + + /** + * Parses a member selector that was specified with a string. + * + * @param classLoader + * Used to resolve class names in the member selectors. Generally you want to pick a class that belongs to + * you application (not to a 3rd party library, like FreeMarker), and then call + * {@link Class#getClassLoader()} on that. Note that the resolution of the classes is not lazy, and so the + * {@link ClassLoader} won't be stored after this method returns. + * @param memberSelectorString + * Describes the member (method, constructor, field) which you want to whitelist. Starts with the full + * qualified name of the member, like {@code com.example.MyClass.myMember}. Unless it's a field, the + * name is followed by comma separated list of the parameter types inside parentheses, like in + * {@code com.example.MyClass.myMember(java.lang.String, boolean)}. The parameter type names must be + * also full qualified names, except primitive type names. Array types must be indicated with one or + * more {@code []}-s after the type name. Varargs arguments shouldn't be marked with {@code ...}, but with + * {@code []}. In the member name, like {@code com.example.MyClass.myMember}, the class refers to the so + * called "upper bound class". Regarding that and inheritance rules see the class level documentation. + * + * @return The {@link MemberSelector}, which might has non-{@code null} {@link MemberSelector#exception}. + */ + public static MemberSelector parse(String memberSelectorString, ClassLoader classLoader) { + if (memberSelectorString.contains("<") || memberSelectorString.contains(">") + || memberSelectorString.contains("...") || memberSelectorString.contains(";")) { + throw new IllegalArgumentException( + "Malformed whitelist entry (shouldn't contain \"<\", \">\", \"...\", or \";\"): " + + memberSelectorString); + } + String cleanedStr = memberSelectorString.trim().replaceAll("\\s*([\\.,\\(\\)\\[\\]])\\s*", "$1"); + + int postMemberNameIdx; + boolean hasArgList; + { + int openParenIdx = cleanedStr.indexOf('('); + hasArgList = openParenIdx != -1; + postMemberNameIdx = hasArgList ? openParenIdx : cleanedStr.length(); + } + + final int postClassDotIdx = cleanedStr.lastIndexOf('.', postMemberNameIdx); + if (postClassDotIdx == -1) { + throw new IllegalArgumentException("Malformed whitelist entry (missing dot): " + memberSelectorString); + } + + Class<?> upperBoundClass; + String upperBoundClassStr = cleanedStr.substring(0, postClassDotIdx); + if (!isWellFormedClassName(upperBoundClassStr)) { + throw new IllegalArgumentException("Malformed whitelist entry (malformed upper bound class name): " + + memberSelectorString); + } + try { + upperBoundClass = classLoader.loadClass(upperBoundClassStr); + } catch (ClassNotFoundException e) { + return new MemberSelector(null, e, cleanedStr); + } + + String memberName = cleanedStr.substring(postClassDotIdx + 1, postMemberNameIdx); + if (!isWellFormedJavaIdentifier(memberName)) { + throw new IllegalArgumentException( + "Malformed whitelist entry (malformed member name): " + memberSelectorString); + } + + if (hasArgList) { + if (cleanedStr.charAt(cleanedStr.length() - 1) != ')') { + throw new IllegalArgumentException("Malformed whitelist entry (missing closing ')'): " + + memberSelectorString); + } + String argsSpec = cleanedStr.substring(postMemberNameIdx + 1, cleanedStr.length() - 1); + StringTokenizer tok = new StringTokenizer(argsSpec, ","); + int argCount = tok.countTokens(); + Class<?>[] argTypes = new Class[argCount]; + for (int i = 0; i < argCount; i++) { + String argClassName = tok.nextToken(); + int arrayDimensions = 0; + while (argClassName.endsWith("[]")) { + arrayDimensions++; + argClassName = argClassName.substring(0, argClassName.length() - 2); + } + Class<?> argClass; + Class<?> primArgClass = ClassUtil.resolveIfPrimitiveTypeName(argClassName); + if (primArgClass != null) { + argClass = primArgClass; + } else { + if (!isWellFormedClassName(argClassName)) { + throw new IllegalArgumentException( + "Malformed whitelist entry (malformed argument class name): " + memberSelectorString); + } + try { + argClass = classLoader.loadClass(argClassName); + } catch (ClassNotFoundException e) { + return new MemberSelector(upperBoundClass, e, cleanedStr); + } catch (SecurityException e) { + return new MemberSelector(upperBoundClass, e, cleanedStr); + } + } + argTypes[i] = ClassUtil.getArrayClass(argClass, arrayDimensions); + } + try { + return memberName.equals(upperBoundClass.getSimpleName()) + ? new MemberSelector(upperBoundClass, upperBoundClass.getConstructor(argTypes)) + : new MemberSelector(upperBoundClass, upperBoundClass.getMethod(memberName, argTypes)); + } catch (NoSuchMethodException e) { + return new MemberSelector(upperBoundClass, e, cleanedStr); + } catch (SecurityException e) { + return new MemberSelector(upperBoundClass, e, cleanedStr); + } + } else { + try { + return new MemberSelector(upperBoundClass, upperBoundClass.getField(memberName)); + } catch (NoSuchFieldException e) { + return new MemberSelector(upperBoundClass, e, cleanedStr); + } catch (SecurityException e) { + return new MemberSelector(upperBoundClass, e, cleanedStr); + } + } + } + + /** + * Convenience method to parse all member selectors in the collection; see {@link #parse(String, ClassLoader)}. + */ + public static List<MemberSelector> parse(Collection<String> memberSelectors, + ClassLoader classLoader) { + List<MemberSelector> parsedMemberSelectors = new ArrayList<MemberSelector>(memberSelectors.size()); + for (String memberSelector : memberSelectors) { + parsedMemberSelectors.add(parse(memberSelector, classLoader)); + } + return parsedMemberSelectors; + } + } + + public WhitelistMemberAccessPolicy(Collection<MemberSelector> memberSelectors) { + methodMatcher = new MethodMatcher(); + constructorMatcher = new ConstructorMatcher(); + fieldMatcher = new FieldMatcher(); + for (MemberSelector memberSelector : memberSelectors) { + Class<?> upperBoundClass = memberSelector.upperBoundType; + if (memberSelector.exception != null) { + if (LOG.isDebugEnabled()) { + LOG.debug("Member selector ignored due to error: " + memberSelector.getExceptionMemberSelectorString(), + memberSelector.exception); + } + } else if (memberSelector.constructor != null) { + constructorMatcher.addMatching(upperBoundClass, memberSelector.constructor); + } else if (memberSelector.method != null) { + methodMatcher.addMatching(upperBoundClass, memberSelector.method); + } else if (memberSelector.field != null) { + fieldMatcher.addMatching(upperBoundClass, memberSelector.field); + } else { + throw new AssertionError(); + } + } + } + + public ClassMemberAccessPolicy forClass(final Class<?> contextClass) { + return new ClassMemberAccessPolicy() { + public boolean isMethodExposed(Method method) { + return methodMatcher.matches(contextClass, method) + || _MethodUtil.getInheritableAnnotation(contextClass, method, TemplateAccessible.class) != null; + } + + public boolean isConstructorExposed(Constructor<?> constructor) { + return constructorMatcher.matches(contextClass, constructor) + || _MethodUtil.getInheritableAnnotation(contextClass, constructor, TemplateAccessible.class) + != null; + } + + public boolean isFieldExposed(Field field) { + return fieldMatcher.matches(contextClass, field) + || _MethodUtil.getInheritableAnnotation(contextClass, field, TemplateAccessible.class) != null; + } + }; + } + + private static boolean isWellFormedClassName(String s) { + if (s.length() == 0) { + return false; + } + int identifierStartIdx = 0; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (i == identifierStartIdx) { + if (!Character.isJavaIdentifierStart(c)) { + return false; + } + } else if (c == '.' && i != s.length() - 1) { + identifierStartIdx = i + 1; + } else { + if (!Character.isJavaIdentifierPart(c)) { + return false; + } + } + } + return true; + } + + private static boolean isWellFormedJavaIdentifier(String s) { + if (s.length() == 0) { + return false; + } + if (!Character.isJavaIdentifierStart(s.charAt(0))) { + return false; + } + for (int i = 1; i < s.length(); i++) { + if (!Character.isJavaIdentifierPart(s.charAt(i))) { + return false; + } + } + return true; + } + +} diff --git a/src/main/java/freemarker/ext/beans/_MethodUtil.java b/src/main/java/freemarker/ext/beans/_MethodUtil.java index 782b944..9f743bc 100644 --- a/src/main/java/freemarker/ext/beans/_MethodUtil.java +++ b/src/main/java/freemarker/ext/beans/_MethodUtil.java @@ -18,7 +18,9 @@ */ package freemarker.ext.beans; +import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; @@ -317,4 +319,143 @@ public final class _MethodUtil { .toString(); } + /** + * Similar to {@link Method#getAnnotation(Class)}, but will also search the annotation in the implemented + * interfaces and in the ancestor classes. + */ + public static <T extends Annotation> T getInheritableAnnotation(Class<?> contextClass, Method method, Class<T> annotationClass) { + T result = method.getAnnotation(annotationClass); + if (result != null) { + return result; + } + return getInheritableMethodAnnotation( + contextClass, method.getName(), method.getParameterTypes(), true, annotationClass); + } + + private static <T extends Annotation> T getInheritableMethodAnnotation( + Class<?> contextClass, String methodName, Class<?>[] methodParamTypes, + boolean skipCheckingDirectMethod, + Class<T> annotationClass) { + if (!skipCheckingDirectMethod) { + Method similarMethod; + try { + similarMethod = contextClass.getMethod(methodName, methodParamTypes); + } catch (NoSuchMethodException e) { + similarMethod = null; + } + if (similarMethod != null) { + T result = similarMethod.getAnnotation(annotationClass); + if (result != null) { + return result; + } + } + } + for (Class<?> anInterface : contextClass.getInterfaces()) { + if (!anInterface.getName().startsWith("java.")) { + Method similarInterfaceMethod; + try { + similarInterfaceMethod = anInterface.getMethod(methodName, methodParamTypes); + } catch (NoSuchMethodException e) { + similarInterfaceMethod = null; + } + if (similarInterfaceMethod != null) { + T result = similarInterfaceMethod.getAnnotation(annotationClass); + if (result != null) { + return result; + } + } + } + } + Class<?> superClass = contextClass.getSuperclass(); + if (superClass == Object.class || superClass == null) { + return null; + } + return getInheritableMethodAnnotation(superClass, methodName, methodParamTypes, false, annotationClass); + } + + /** + * Similar to {@link Constructor#getAnnotation(Class)}, but will also search the annotation in the implemented + * interfaces and in the ancestor classes. + */ + public static <T extends Annotation> T getInheritableAnnotation( + Class<?> contextClass, Constructor<?> constructor, Class<T> annotationClass) { + T result = constructor.getAnnotation(annotationClass); + if (result != null) { + return result; + } + + Class<?>[] paramTypes = constructor.getParameterTypes(); + while (true) { + contextClass = contextClass.getSuperclass(); + if (contextClass == Object.class || contextClass == null) { + return null; + } + try { + constructor = contextClass.getConstructor(paramTypes); + } catch (NoSuchMethodException e) { + constructor = null; + } + if (constructor != null) { + result = constructor.getAnnotation(annotationClass); + if (result != null) { + return result; + } + } + } + } + + /** + * Similar to {@link Field#getAnnotation(Class)}, but will also search the annotation in the implemented + * interfaces and in the ancestor classes. + */ + public static <T extends Annotation> T getInheritableAnnotation(Class<?> contextClass, Field field, Class<T> annotationClass) { + T result = field.getAnnotation(annotationClass); + if (result != null) { + return result; + } + return getInheritableFieldAnnotation( + contextClass, field.getName(), true, annotationClass); + } + + private static <T extends Annotation> T getInheritableFieldAnnotation( + Class<?> contextClass, String fieldName, + boolean skipCheckingDirectField, + Class<T> annotationClass) { + if (!skipCheckingDirectField) { + Field similarField; + try { + similarField = contextClass.getField(fieldName); + } catch (NoSuchFieldException e) { + similarField = null; + } + if (similarField != null) { + T result = similarField.getAnnotation(annotationClass); + if (result != null) { + return result; + } + } + } + for (Class<?> anInterface : contextClass.getInterfaces()) { + if (!anInterface.getName().startsWith("java.")) { + Field similarInterfaceField; + try { + similarInterfaceField = anInterface.getField(fieldName); + } catch (NoSuchFieldException e) { + similarInterfaceField = null; + } + if (similarInterfaceField != null) { + T result = similarInterfaceField.getAnnotation(annotationClass); + if (result != null) { + return result; + } + } + } + } + Class<?> superClass = contextClass.getSuperclass(); + if (superClass == Object.class || superClass == null) { + return null; + } + return getInheritableFieldAnnotation(superClass, fieldName, false, annotationClass); + } + } \ No newline at end of file diff --git a/src/main/java/freemarker/template/DefaultObjectWrapper.java b/src/main/java/freemarker/template/DefaultObjectWrapper.java index 4c5a39d..8333a00 100644 --- a/src/main/java/freemarker/template/DefaultObjectWrapper.java +++ b/src/main/java/freemarker/template/DefaultObjectWrapper.java @@ -32,6 +32,7 @@ import org.w3c.dom.Node; import freemarker.ext.beans.BeansWrapper; import freemarker.ext.beans.BeansWrapperConfiguration; +import freemarker.ext.beans.DefaultMemberAccessPolicy; import freemarker.ext.beans.EnumerationModel; import freemarker.ext.dom.NodeModel; import freemarker.log.Logger; @@ -252,7 +253,8 @@ public class DefaultObjectWrapper extends freemarker.ext.beans.BeansWrapper { * Called for an object that isn't considered to be of a "basic" Java type, like for an application specific type, * or for a W3C DOM node. In its default implementation, W3C {@link Node}-s will be wrapped as {@link NodeModel}-s * (allows DOM tree traversal), Jython objects will be delegated to the {@code JythonWrapper}, others will be - * wrapped using {@link BeansWrapper#wrap(Object)}. + * wrapped using {@link BeansWrapper#wrap(Object)}. Note that if {@link #getMemberAccessPolicy()} doesn't return + * a {@link DefaultMemberAccessPolicy}, then Jython wrapper will be skipped for security reasons. * * <p> * When you override this method, you should first decide if you want to wrap the object in a custom way (and if so @@ -263,8 +265,10 @@ public class DefaultObjectWrapper extends freemarker.ext.beans.BeansWrapper { if (obj instanceof Node) { return wrapDomNode(obj); } - if (JYTHON_WRAPPER != null && JYTHON_OBJ_CLASS.isInstance(obj)) { - return JYTHON_WRAPPER.wrap(obj); + if (getMemberAccessPolicy() instanceof DefaultMemberAccessPolicy) { + if (JYTHON_WRAPPER != null && JYTHON_OBJ_CLASS.isInstance(obj)) { + return JYTHON_WRAPPER.wrap(obj); + } } return super.wrap(obj); } diff --git a/src/main/java/freemarker/template/utility/ClassUtil.java b/src/main/java/freemarker/template/utility/ClassUtil.java index ad19750..95fd3b8 100644 --- a/src/main/java/freemarker/template/utility/ClassUtil.java +++ b/src/main/java/freemarker/template/utility/ClassUtil.java @@ -21,8 +21,11 @@ package freemarker.template.utility; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Array; import java.net.URL; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Properties; import java.util.Set; @@ -88,7 +91,39 @@ public class ClassUtil { // Fall back to the defining class loader of the FreeMarker classes return Class.forName(className); } - + + private static final Map<String, Class<?>> PRIMITIVE_CLASSES_BY_NAME; + static { + PRIMITIVE_CLASSES_BY_NAME = new HashMap<String, Class<?>>(); + PRIMITIVE_CLASSES_BY_NAME.put("boolean", boolean.class); + PRIMITIVE_CLASSES_BY_NAME.put("byte", byte.class); + PRIMITIVE_CLASSES_BY_NAME.put("char", char.class); + PRIMITIVE_CLASSES_BY_NAME.put("short", short.class); + PRIMITIVE_CLASSES_BY_NAME.put("int", int.class); + PRIMITIVE_CLASSES_BY_NAME.put("long", long.class); + PRIMITIVE_CLASSES_BY_NAME.put("float", float.class); + PRIMITIVE_CLASSES_BY_NAME.put("double", double.class); + } + + /** + * Returns the {@link Class} for a primitive type name, or {@code null} if it's not the name of a primitive type. + * + * @since 2.3.30 + */ + public static Class<?> resolveIfPrimitiveTypeName(String typeName) { + return PRIMITIVE_CLASSES_BY_NAME.get(typeName); + } + + /** + * Returns the array type that corresponds to the element type and the given number of array dimensions. + * If the dimension is 0, it just returns the element type as is. + * + * @since 2.3.30 + */ + public static Class<?> getArrayClass(Class<?> elementType, int dimensions) { + return dimensions == 0 ? elementType : Array.newInstance(elementType, new int[dimensions]).getClass(); + } + /** * Same as {@link #getShortClassName(Class, boolean) getShortClassName(pClass, false)}. * diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml index 4b10a92..a2732b6 100644 --- a/src/manual/en_US/book.xml +++ b/src/manual/en_US/book.xml @@ -20,10 +20,7 @@ <book conformance="docgen" version="5.0" xml:lang="en" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns:ns5="http://www.w3.org/2000/svg" - xmlns:ns4="http://www.w3.org/1998/Math/MathML" - xmlns:ns3="http://www.w3.org/1999/xhtml" - xmlns:ns="http://docbook.org/ns/docbook"> +> <info> <title>Apache FreeMarker Manual</title> @@ -28887,10 +28884,11 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> <answer> <para>In general you shouldn't allow that, unless those users are - system administrators or other trusted personnel. Consider - templates as part of the source code just like - <literal>*.java</literal> files are. If you still want to allow - users to upload templates, here are what to consider:</para> + application developers, system administrators, or other highly + trusted personnel. Consider templates as part of the source code + just like <literal>*.java</literal> files are. If you still want + to allow users to upload templates, here's what to + consider:</para> <itemizedlist> <listitem> @@ -28915,11 +28913,17 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> <literal>List</literal>-s, <literal>Array</literal>-s, <literal>String</literal>-s, <literal>Number</literal>-s, <literal>Boolean</literal>-s and <literal>Date</literal>-s. - For many application though that's too restrictive, and - instead you need to implement your own extremely restrictive - <literal>ObjectWrapper</literal>, which, for example, only - exposes those members of POJO-s that were explicitly marked to - be safe (opt-in approach).</para> + But for many application that's too restrictive, and instead + you have to create a + <literal>WhitelistMemberAccessPolicy</literal>, and create a + <literal>DefaultObjectWrapper</literal> (or other + <literal>BeansWrapper</literal> subclass that you would use) + that uses that. See the Java API documentation of + <literal>WhitelistMemberAccessPolicy</literal> for more. (Or, + you can roll your own <literal>MemberAccessPolicy</literal> + implementation, or even your own restrictive + <literal>ObjectWrapper</literal> implementation of + course.)</para> <para>Also, don't forget about the <link linkend="ref_buitin_api_and_has_api"><literal>?api</literal> @@ -28935,16 +28939,23 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> the <literal>ObjectWrapper</literal> is still in control, as it decides what objects support <literal>?api</literal>, and what will <literal>?api</literal> expose for them (it usually - exposes the same as for a generic POJO).</para> + exposes the same as for a generic POJO). Members not allowed + by the <literal>MemberAccessPolicy</literal> also won't be + visible with <literal>?api</literal> (assuming you are using a + well behaving <literal>ObjectWrapper</literal>, like + <literal>DefaultObjectWrapper</literal> is, hopefully.) + </para> <para>Last not least, some maybe aware of that the standard object wrappers filters out some well known <quote>unsafe</quote> methods, like - <literal>System.exit</literal>. Do not ever rely on this as - your only line of defense, since it only blocks the methods - that's in a predefined list. Thus, for example, if a new Java - version adds a new problematic method, it won't be filtered - out.</para> + <literal>System.exit</literal>. Do not ever rely on that, + since it only blocks the methods that's in a small predefined + list (for some historical reasons). The standard Java API is + huge and ever growing, and then there are the 3rd party + libraries, and the API-s of your own application. Clearly it's + impossible to blacklist all the problematic members in + those.</para> </listitem> <listitem> @@ -28986,7 +28997,13 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> <literal>TemplateClassResolver</literal> that restricts the accessible classes (possibly based on which template asks for them), such as - <literal>TemplateClassResolver.ALLOWS_NOTHING_RESOLVER</literal>.</para> + <literal>TemplateClassResolver.ALLOWS_NOTHING_RESOLVER</literal>. + Note that if, and only if your + <literal>ObjectWrapper</literal> is a + <literal>BeansWrapper</literal> or a subclass of it (typically + <literal>DefaultObjectWrapper</literal>), constructors not + allowed by the <literal>MemberAccessPolicy</literal> also + won't be accessible for <literal>?new</literal>. </para> </listitem> <listitem> @@ -29197,6 +29214,23 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> <listitem> <para>Added + <literal>freemarker.ext.beans.WhitelistMemberAccessPolicy</literal>, + which is a <literal>MemberAccessPolicy</literal> for use cases + where you want to allow editing templates to users who shouldn't + have the same rights as the developers (the same rights as the + Java application). Earlier, the only out of the box solution for + that was <literal>SimpleObjectWrapper</literal>, but that's too + restrictive for most applications where FreeMarker is used. + <literal>WhitelistMemberAccessPolicy</literal> works with + <literal>DefaultObjectWrapper</literal> (or any other + <literal>BeansWrapper</literal>), allowing you to use all + features of it, but it will only allow accessing members that + were explicitly listed by the developers, or was annotated with + <literal>@TemplateAccessible</literal>.</para> + </listitem> + + <listitem> + <para>Added <literal>Environment.getDataModelOrSharedVariable(String)</literal>.</para> </listitem> diff --git a/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java b/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java index 355a769..eafbaf2 100644 --- a/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java +++ b/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java @@ -22,24 +22,32 @@ package freemarker.ext.beans; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import java.io.IOException; +import java.io.StringWriter; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import org.junit.Test; +import com.google.common.collect.ImmutableMap; + import freemarker.template.Configuration; import freemarker.template.DefaultObjectWrapper; import freemarker.template.DefaultObjectWrapperBuilder; import freemarker.template.ObjectWrapperAndUnwrapper; import freemarker.template.SimpleNumber; +import freemarker.template.Template; +import freemarker.template.TemplateException; import freemarker.template.TemplateHashModel; import freemarker.template.TemplateMethodModelEx; import freemarker.template.TemplateModel; import freemarker.template.TemplateModelException; +import freemarker.template.TemplateNumberModel; public class DefaultObjectWrapperMemberAccessPolicyTest { @@ -166,7 +174,7 @@ public class DefaultObjectWrapperMemberAccessPolicyTest { public void testMethodsWithCustomMemberAccessPolicy() throws TemplateModelException { DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30); owb.setMemberAccessPolicy(new MemberAccessPolicy() { - public ClassMemberAccessPolicy forClass(Class<?> containingClass) { + public ClassMemberAccessPolicy forClass(Class<?> contextClass) { return new ClassMemberAccessPolicy() { public boolean isMethodExposed(Method method) { String name = method.getName(); @@ -208,7 +216,7 @@ public class DefaultObjectWrapperMemberAccessPolicyTest { DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30); owb.setExposeFields(true); owb.setMemberAccessPolicy(new MemberAccessPolicy() { - public ClassMemberAccessPolicy forClass(Class<?> containingClass) { + public ClassMemberAccessPolicy forClass(Class<?> contextClass) { return new ClassMemberAccessPolicy() { public boolean isMethodExposed(Method method) { return true; @@ -238,7 +246,7 @@ public class DefaultObjectWrapperMemberAccessPolicyTest { public void testGenericGetWithCustomMemberAccessPolicy() throws TemplateModelException { DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30); owb.setMemberAccessPolicy(new MemberAccessPolicy() { - public ClassMemberAccessPolicy forClass(Class<?> containingClass) { + public ClassMemberAccessPolicy forClass(Class<?> contextClass) { return new ClassMemberAccessPolicy() { public boolean isMethodExposed(Method method) { return false; @@ -264,7 +272,7 @@ public class DefaultObjectWrapperMemberAccessPolicyTest { public void testConstructorsWithCustomMemberAccessPolicy() throws TemplateModelException { DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30); owb.setMemberAccessPolicy(new MemberAccessPolicy() { - public ClassMemberAccessPolicy forClass(Class<?> containingClass) { + public ClassMemberAccessPolicy forClass(Class<?> contextClass) { return new ClassMemberAccessPolicy() { public boolean isMethodExposed(Method method) { return true; @@ -305,6 +313,93 @@ public class DefaultObjectWrapperMemberAccessPolicyTest { Collections.singletonList(new SimpleNumber(1))).getClass()); } + @Test + public void testMemberAccessPolicyAndApiBI() throws IOException, TemplateException { + DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30); + owb.setMemberAccessPolicy(new MemberAccessPolicy() { + public ClassMemberAccessPolicy forClass(Class<?> contextClass) { + return new ClassMemberAccessPolicy() { + public boolean isMethodExposed(Method method) { + return method.getName().equals("size"); + } + + public boolean isConstructorExposed(Constructor<?> constructor) { + return true; + } + + public boolean isFieldExposed(Field field) { + return true; + } + }; + } + }); + DefaultObjectWrapper ow = owb.build(); + + Map<String, Object> dataModel = ImmutableMap.<String, Object>of("m", ImmutableMap.of("k", "v")); + + Configuration cfg = new Configuration(Configuration.VERSION_2_3_30); + cfg.setObjectWrapper(ow); + cfg.setAPIBuiltinEnabled(true); + Template template = new Template(null, "size=${m?api.size()} get=${(m?api.get('k'))!'hidden'}", cfg); + + { + StringWriter out = new StringWriter(); + template.process(dataModel, out); + assertEquals("size=1 get=hidden", out.toString()); + } + + cfg.setObjectWrapper(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30).build()); + { + StringWriter out = new StringWriter(); + template.process(dataModel, out); + assertEquals("size=1 get=v", out.toString()); + } + } + + @Test + public void testMemberAccessPolicyAndNewBI() throws IOException, TemplateException { + DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30); + owb.setMemberAccessPolicy(new MemberAccessPolicy() { + public ClassMemberAccessPolicy forClass(Class<?> contextClass) { + return new ClassMemberAccessPolicy() { + public boolean isMethodExposed(Method method) { + return true; + } + + public boolean isConstructorExposed(Constructor<?> constructor) { + return constructor.getDeclaringClass().equals(CustomModel.class); + } + + public boolean isFieldExposed(Field field) { + return true; + } + }; + } + }); + DefaultObjectWrapper ow = owb.build(); + + Configuration cfg = new Configuration(Configuration.VERSION_2_3_30); + cfg.setObjectWrapper(ow); + cfg.setAPIBuiltinEnabled(true); + Template template = new Template(null, + "${'" + CustomModel.class.getName() + "'?new()} " + + "<#attempt>${'" + OtherCustomModel.class.getName() + "'?new()}<#recover>failed</#attempt>", + cfg); + + { + StringWriter out = new StringWriter(); + template.process(null, out); + assertEquals("1 failed", out.toString()); + } + + cfg.setObjectWrapper(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30).build()); + { + StringWriter out = new StringWriter(); + template.process(null, out); + assertEquals("1 2", out.toString()); + } + } + private static DefaultObjectWrapper createDefaultMemberAccessPolicyObjectWrapper() { return new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30).build(); } @@ -406,4 +501,16 @@ public class DefaultObjectWrapperMemberAccessPolicyTest { } } + public static class CustomModel implements TemplateNumberModel { + public Number getAsNumber() { + return 1; + } + } + + public static class OtherCustomModel implements TemplateNumberModel { + public Number getAsNumber() { + return 2; + } + } + } diff --git a/src/test/java/freemarker/ext/beans/MethodMatcherTest.java b/src/test/java/freemarker/ext/beans/MethodMatcherTest.java new file mode 100644 index 0000000..9ac9ca9 --- /dev/null +++ b/src/test/java/freemarker/ext/beans/MethodMatcherTest.java @@ -0,0 +1,179 @@ +/* + * 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.ext.beans; + +import static org.junit.Assert.*; + +import java.lang.reflect.Method; + +import org.junit.Test; + +public class MethodMatcherTest { + + @Test + public void testReturnTypeOverload() throws NoSuchMethodException { + MethodMatcher matcher = new MethodMatcher(); + Method genericM = TestReturnTypeOverloadGeneric.class.getMethod("m"); + assertEquals(Object.class, genericM.getReturnType()); + matcher.addMatching(TestReturnTypeOverloadGeneric.class, genericM); + + Method stringM = TestReturnTypeOverloadString.class.getMethod("m"); + assertEquals(String.class, stringM.getReturnType()); + + assertTrue(matcher.matches(TestReturnTypeOverloadGeneric.class, genericM)); + assertTrue(matcher.matches(TestReturnTypeOverloadString.class, genericM)); + assertTrue(matcher.matches(TestReturnTypeOverloadString.class, stringM)); + } + + public static class TestReturnTypeOverloadGeneric<T> { + public T m() { + return null; + }; + } + + public static class TestReturnTypeOverloadString extends TestReturnTypeOverloadGeneric<String> { + public String m() { + return ""; + }; + } + + /** Mostly to test upper bound classes. */ + @Test + public void testInheritance() throws NoSuchMethodException { + { + MethodMatcher matcher = new MethodMatcher(); + Method m = TestInheritanceC2.class.getMethod("m1"); + assertEquals(m, TestInheritanceC1.class.getMethod("m1")); + matcher.addMatching(TestInheritanceC2.class, m); + assertFalse(matcher.matches(TestInheritanceC1.class, m)); + assertTrue(matcher.matches(TestInheritanceC2.class, m)); + assertTrue(matcher.matches(TestInheritanceC3.class, m)); + } + { + MethodMatcher matcher = new MethodMatcher(); + Method m = TestInheritanceC2.class.getMethod("m2"); + assertNotEquals(m, TestInheritanceC1.class.getMethod("m2")); + matcher.addMatching(TestInheritanceC2.class, m); + assertFalse(matcher.matches(TestInheritanceC1.class, m)); + assertTrue(matcher.matches(TestInheritanceC2.class, m)); + assertTrue(matcher.matches(TestInheritanceC3.class, m)); + } + { + // m2 again, but with a non-same-instance but "equal" method. + MethodMatcher matcher = new MethodMatcher(); + Method m = TestInheritanceC1.class.getMethod("m2"); + matcher.addMatching(TestInheritanceC2.class, m); + assertFalse(matcher.matches(TestInheritanceC1.class, m)); + assertTrue(matcher.matches(TestInheritanceC2.class, m)); + assertTrue(matcher.matches(TestInheritanceC3.class, m)); + } + { + MethodMatcher matcher = new MethodMatcher(); + Method m = TestInheritanceC2.class.getMethod("m3"); + assertEquals(m, TestInheritanceC1.class.getMethod("m3")); + assertNotEquals(m, TestInheritanceC3.class.getMethod("m3")); + matcher.addMatching(TestInheritanceC2.class, m); + assertFalse(matcher.matches(TestInheritanceC1.class, m)); + assertTrue(matcher.matches(TestInheritanceC2.class, m)); + assertTrue(matcher.matches(TestInheritanceC3.class, m)); + } + } + + public static class TestInheritanceC1 { + public void m1() { + } + + public void m2() { + } + + public void m3() { + } + } + + public static class TestInheritanceC2 extends TestInheritanceC1 { + @Override + public void m2() { + } + } + + public static class TestInheritanceC3 extends TestInheritanceC2 { + @Override + public void m3() { + } + } + + /** Mostly to test when same method associated to multiple unrelated classes. */ + @Test + public void testInheritance2() throws NoSuchMethodException { + MethodMatcher matcher = new MethodMatcher(); + Method m = Runnable.class.getMethod("run"); + matcher.addMatching(TestInheritance2SafeRunnable1.class, m); + matcher.addMatching(TestInheritance2SafeRunnable2.class, m); + + assertTrue(matcher.matches( + TestInheritance2SafeRunnable1.class, TestInheritance2SafeRunnable1.class.getMethod("run"))); + assertTrue(matcher.matches( + TestInheritance2SafeRunnable2.class, TestInheritance2SafeRunnable2.class.getMethod("run"))); + assertFalse(matcher.matches( + TestInheritance2UnsafeRunnable.class, TestInheritance2UnsafeRunnable.class.getMethod("run"))); + } + + public static class TestInheritance2SafeRunnable1 implements Runnable { + public void run() { + } + } + + public static class TestInheritance2SafeRunnable2 implements Runnable { + public void run() { + } + } + + public static class TestInheritance2UnsafeRunnable implements Runnable { + public void run() { + } + } + + @Test + public void testOverloads() throws NoSuchMethodException { + Method mInt = TestOverloads.class.getMethod("m", int.class); + Method mIntInt = TestOverloads.class.getMethod("m", int.class, int.class); + { + MethodMatcher matcher = new MethodMatcher(); + matcher.addMatching(TestOverloads.class, mInt); + assertTrue(matcher.matches(TestOverloads.class, mInt)); + assertFalse(matcher.matches(TestOverloads.class, mIntInt)); + } + { + MethodMatcher matcher = new MethodMatcher(); + matcher.addMatching(TestOverloads.class, mIntInt); + assertFalse(matcher.matches(TestOverloads.class, mInt)); + assertTrue(matcher.matches(TestOverloads.class, mIntInt)); + } + } + + public static class TestOverloads { + public void m(int x) { + } + + public void m(int x, int y) { + } + } + +} diff --git a/src/test/java/freemarker/ext/beans/MethodUtilTest.java b/src/test/java/freemarker/ext/beans/MethodUtilTest.java new file mode 100644 index 0000000..24291b3 --- /dev/null +++ b/src/test/java/freemarker/ext/beans/MethodUtilTest.java @@ -0,0 +1,156 @@ +/* + * 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.ext.beans; + +import static org.junit.Assert.*; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.junit.Test; + +public class MethodUtilTest { + + @Test + public void testMethodBasic() throws NoSuchMethodException, NoSuchFieldException { + assertNotNull(_MethodUtil.getInheritableAnnotation( + C1.class, C1.class.getMethod("m1"), TemplateAccessible.class)); + assertNull(_MethodUtil.getInheritableAnnotation( + C1.class, C1.class.getMethod("m2"), TemplateAccessible.class)); + + assertNotNull(_MethodUtil.getInheritableAnnotation( + C1.class, C1.class.getConstructor(int.class), TemplateAccessible.class)); + assertNull(_MethodUtil.getInheritableAnnotation( + C1.class, C1.class.getConstructor(int.class, int.class), TemplateAccessible.class)); + + assertNotNull(_MethodUtil.getInheritableAnnotation( + C1.class, C1.class.getField("f1"), TemplateAccessible.class)); + assertNull(_MethodUtil.getInheritableAnnotation( + C1.class, C1.class.getField("f3"), TemplateAccessible.class)); + } + + @Test + public void testMethodInheritance() throws NoSuchMethodException, NoSuchFieldException { + assertNotNull(_MethodUtil.getInheritableAnnotation( + C2.class, C2.class.getMethod("m1"), TemplateAccessible.class)); + assertNotNull(_MethodUtil.getInheritableAnnotation( + C2.class, C2.class.getMethod("m2"), TemplateAccessible.class)); + assertNotNull(_MethodUtil.getInheritableAnnotation( + C2.class, C2.class.getMethod("m3"), TemplateAccessible.class)); + assertNotNull(_MethodUtil.getInheritableAnnotation( + C2.class, C2.class.getMethod("m4"), TemplateAccessible.class)); + assertNotNull(_MethodUtil.getInheritableAnnotation( + C2.class, C2.class.getMethod("m5"), TemplateAccessible.class)); + + assertNotNull(_MethodUtil.getInheritableAnnotation( + C2.class, C2.class.getConstructor(int.class), TemplateAccessible.class)); + assertNull(_MethodUtil.getInheritableAnnotation( + C2.class, C2.class.getConstructor(), TemplateAccessible.class)); + + assertNotNull(_MethodUtil.getInheritableAnnotation( + C2.class, C2.class.getField("f1"), TemplateAccessible.class)); + assertNotNull(_MethodUtil.getInheritableAnnotation( + C2.class, C2.class.getField("f2"), TemplateAccessible.class)); + assertNull(_MethodUtil.getInheritableAnnotation( + C2.class, C2.class.getField("f3"), TemplateAccessible.class)); + assertNotNull(_MethodUtil.getInheritableAnnotation( + C2.class, C2.class.getField("f4"), TemplateAccessible.class)); + } + + @Test + public void testMethodInheritanceWithSyntheticMethod() { + for (Method method : D2.class.getMethods()) { + if (method.getName().equals("m1")) { + assertNotNull(_MethodUtil.getInheritableAnnotation( + C2.class, method, TemplateAccessible.class)); + } + } + } + + static public class C1 implements Serializable { + @TemplateAccessible + public int f1; + + @TemplateAccessible + public int f2; + + public int f3; + + public int f4; + + @TemplateAccessible + public C1(int x) {} + + public C1(int x, int y) {} + + @TemplateAccessible + public void m1() {} + + public void m2() {} + + public void m3() {} + + @TemplateAccessible + public void m4() {} + + @TemplateAccessible + public void m5() {} + } + + static public class C2 extends C1 implements I1 { + public long f2; + + public C2() { + super(0); + } + + public C2(int x) { + super(x); + } + + @Override + public void m1() {} + + @TemplateAccessible + @Override + public void m3() {} + } + + public interface I1 { + @TemplateAccessible + int f4 = 0; + + @TemplateAccessible + void m2(); + + void m5(); + } + + public static class D1<T> { + @TemplateAccessible + public T m1() { return null; } + } + + public static class D2 extends D1<String> { + @Override + public String m1() { return ""; } + } + +} \ No newline at end of file diff --git a/src/test/java/freemarker/ext/beans/WhitelistMemberAccessPolicyTest.java b/src/test/java/freemarker/ext/beans/WhitelistMemberAccessPolicyTest.java new file mode 100644 index 0000000..0edee07 --- /dev/null +++ b/src/test/java/freemarker/ext/beans/WhitelistMemberAccessPolicyTest.java @@ -0,0 +1,558 @@ +/* + * 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.ext.beans; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.io.Serializable; +import java.util.Arrays; + +import org.junit.Test; + +import freemarker.template.Configuration; +import freemarker.template.TemplateException; +import freemarker.template.TemplateHashModel; + +public class WhitelistMemberAccessPolicyTest { + + @Test + public void testEmpty() throws NoSuchMethodException, NoSuchFieldException { + WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(); + ClassMemberAccessPolicy classPolicy = policy.forClass(C1.class); + assertFalse(classPolicy.isConstructorExposed(C1.class.getConstructor())); + assertFalse(classPolicy.isMethodExposed(C1.class.getMethod("m1"))); + assertFalse(classPolicy.isFieldExposed(C1.class.getField("f1"))); + } + + @Test + public void testBasics() throws NoSuchMethodException, NoSuchFieldException { + WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy( + C1.class.getName() + "." + C1.class.getSimpleName() + "()", + C1.class.getName() + ".m1()", + C1.class.getName() + ".m2(int)", + C1.class.getName() + ".f1"); + + { + ClassMemberAccessPolicy c1Policy = policy.forClass(C1.class); + assertTrue(c1Policy.isConstructorExposed(C1.class.getConstructor())); + assertTrue(c1Policy.isMethodExposed(C1.class.getMethod("m1"))); + assertTrue(c1Policy.isMethodExposed(C1.class.getMethod("m2", int.class))); + assertTrue(c1Policy.isFieldExposed(C1.class.getField("f1"))); + } + + { + ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class); + assertFalse(d1Policy.isMethodExposed(D1.class.getMethod("m1"))); + assertFalse(d1Policy.isFieldExposed(D1.class.getField("f1"))); + } + } + + @Test + public void testInheritanceAndMoreOverloads() throws NoSuchMethodException, NoSuchFieldException { + WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy( + C1.class.getName() + ".m2(int)", + C1.class.getName() + ".f1", + C2.class.getName() + "." + C2.class.getSimpleName() + "(int)", + C2.class.getName() + ".m1()", + C2.class.getName() + ".m2(boolean)", + C3.class.getName() + ".f2", + C3.class.getName() + "." + C3.class.getSimpleName() + "()", + C3.class.getName() + ".m4()", + C3.class.getName() + ".f3" + ); + ClassMemberAccessPolicy c1Policy = policy.forClass(C1.class); + ClassMemberAccessPolicy c2Policy = policy.forClass(C2.class); + ClassMemberAccessPolicy c3Policy = policy.forClass(C3.class); + + assertTrue(c1Policy.isMethodExposed(C1.class.getMethod("m2", int.class))); + assertTrue(c2Policy.isMethodExposed(C2.class.getMethod("m2", int.class))); + assertTrue(c3Policy.isMethodExposed(C3.class.getMethod("m2", int.class))); + + assertTrue(c1Policy.isFieldExposed(C1.class.getField("f1"))); + assertTrue(c2Policy.isFieldExposed(C2.class.getField("f1"))); + assertTrue(c3Policy.isFieldExposed(C3.class.getField("f1"))); + + assertFalse(c1Policy.isConstructorExposed(C1.class.getConstructor(int.class))); + assertTrue(c2Policy.isConstructorExposed(C2.class.getConstructor(int.class))); + assertTrue(c3Policy.isConstructorExposed(C3.class.getConstructor(int.class))); + + assertFalse(c1Policy.isMethodExposed(C1.class.getMethod("m1"))); + assertTrue(c2Policy.isMethodExposed(C2.class.getMethod("m1"))); + assertTrue(c3Policy.isMethodExposed(C3.class.getMethod("m1"))); + + assertFalse(c1Policy.isMethodExposed(C2.class.getMethod("m2", boolean.class))); // Doesn't exist in C1 + assertTrue(c2Policy.isMethodExposed(C2.class.getMethod("m2", boolean.class))); + assertTrue(c3Policy.isMethodExposed(C3.class.getMethod("m2", boolean.class))); + + assertFalse(c1Policy.isFieldExposed(C1.class.getField("f2"))); + assertFalse(c2Policy.isFieldExposed(C2.class.getField("f2"))); + assertTrue(c3Policy.isFieldExposed(C3.class.getField("f2"))); + + assertFalse(c1Policy.isConstructorExposed(C1.class.getConstructor())); + assertFalse(c2Policy.isConstructorExposed(C1.class.getConstructor())); // Doesn't exist in C2 + assertTrue(c3Policy.isConstructorExposed(C3.class.getConstructor())); + + assertFalse(c1Policy.isMethodExposed(C2.class.getMethod("m4"))); // Doesn't exist in C1 + assertFalse(c2Policy.isMethodExposed(C2.class.getMethod("m4"))); + assertTrue(c3Policy.isMethodExposed(C3.class.getMethod("m4"))); + + assertFalse(c1Policy.isFieldExposed(C2.class.getField("f3"))); // Doesn't exist in C1 + assertFalse(c2Policy.isFieldExposed(C2.class.getField("f3"))); + assertTrue(c3Policy.isFieldExposed(C3.class.getField("f3"))); + } + + @Test + public void testInterfaces() throws NoSuchMethodException, NoSuchFieldException { + { + WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy( + I1.class.getName() + ".m1()", + I1.class.getName() + ".f1" + ); + ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class); + ClassMemberAccessPolicy d2Policy = policy.forClass(D2.class); + ClassMemberAccessPolicy e1Policy = policy.forClass(E1.class); + ClassMemberAccessPolicy e2Policy = policy.forClass(E2.class); + assertTrue(d1Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertTrue(d2Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertTrue(e1Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertTrue(e2Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertTrue(d1Policy.isFieldExposed(I1.class.getField("f1"))); + assertTrue(d2Policy.isFieldExposed(I1.class.getField("f1"))); + assertTrue(e1Policy.isFieldExposed(I1.class.getField("f1"))); + assertTrue(e2Policy.isFieldExposed(I1.class.getField("f1"))); + } + { + WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy( + I1Sub.class.getName() + ".m1()", + I1Sub.class.getName() + ".m2()", + I1Sub.class.getName() + ".f1" + ); + ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class); + ClassMemberAccessPolicy d2Policy = policy.forClass(D2.class); + ClassMemberAccessPolicy e1Policy = policy.forClass(E1.class); + ClassMemberAccessPolicy e2Policy = policy.forClass(E2.class); + assertFalse(d1Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertFalse(d2Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertTrue(e1Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertTrue(e2Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertFalse(d1Policy.isMethodExposed(I1Sub.class.getMethod("m2"))); + assertFalse(d2Policy.isMethodExposed(I1Sub.class.getMethod("m2"))); + assertTrue(e1Policy.isMethodExposed(I1Sub.class.getMethod("m2"))); + assertTrue(e2Policy.isMethodExposed(I1Sub.class.getMethod("m2"))); + assertFalse(d1Policy.isFieldExposed(I1.class.getField("f1"))); + assertFalse(d2Policy.isFieldExposed(I1.class.getField("f1"))); + assertTrue(e1Policy.isFieldExposed(I1.class.getField("f1"))); + assertTrue(e2Policy.isFieldExposed(I1.class.getField("f1"))); + } + { + WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy( + I1.class.getName() + ".m1()", + I1.class.getName() + ".f1" + ); + ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class); + ClassMemberAccessPolicy d2Policy = policy.forClass(D2.class); + ClassMemberAccessPolicy e1Policy = policy.forClass(E1.class); + ClassMemberAccessPolicy e2Policy = policy.forClass(E2.class); + assertTrue(d1Policy.isMethodExposed(I1Sub.class.getMethod("m1"))); + assertTrue(d2Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertTrue(e1Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertTrue(e2Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertFalse(d1Policy.isMethodExposed(I1Sub.class.getMethod("m2"))); + assertFalse(d2Policy.isMethodExposed(I1Sub.class.getMethod("m2"))); + assertFalse(e1Policy.isMethodExposed(I1Sub.class.getMethod("m2"))); + assertFalse(e2Policy.isMethodExposed(I1Sub.class.getMethod("m2"))); + assertTrue(d1Policy.isFieldExposed(I1Sub.class.getField("f1"))); + assertTrue(d2Policy.isFieldExposed(I1.class.getField("f1"))); + assertTrue(e1Policy.isFieldExposed(I1.class.getField("f1"))); + assertTrue(e2Policy.isFieldExposed(I1.class.getField("f1"))); + } + { + WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy( + D2.class.getName() + ".m1()", + D2.class.getName() + ".f1" + ); + ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class); + ClassMemberAccessPolicy d2Policy = policy.forClass(D2.class); + assertFalse(d1Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertTrue(d2Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertFalse(d1Policy.isFieldExposed(I1.class.getField("f1"))); + assertTrue(d2Policy.isFieldExposed(I1.class.getField("f1"))); + } + { + WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy( + I1Sub.class.getName() + ".m1()", + D2.class.getName() + ".m1()", + I1Sub.class.getName() + ".m2()", + J1.class.getName() + ".m2()", + I1.class.getName() + ".f1", + I1Sub.class.getName() + ".f1" + ); + ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class); + ClassMemberAccessPolicy d2Policy = policy.forClass(D2.class); + ClassMemberAccessPolicy e1Policy = policy.forClass(E1.class); + ClassMemberAccessPolicy e2Policy = policy.forClass(E2.class); + ClassMemberAccessPolicy f1Policy = policy.forClass(F1.class); + assertFalse(d1Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertTrue(d2Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertTrue(e1Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertTrue(e2Policy.isMethodExposed(I1.class.getMethod("m1"))); + assertFalse(d1Policy.isMethodExposed(J1.class.getMethod("m2"))); + assertFalse(d2Policy.isMethodExposed(J1.class.getMethod("m2"))); + assertTrue(e1Policy.isMethodExposed(J1.class.getMethod("m2"))); + assertTrue(e2Policy.isMethodExposed(J1.class.getMethod("m2"))); + assertTrue(f1Policy.isMethodExposed(J1.class.getMethod("m2"))); + assertTrue(d1Policy.isFieldExposed(I1.class.getField("f1"))); + assertTrue(d2Policy.isFieldExposed(I1.class.getField("f1"))); + assertTrue(e1Policy.isFieldExposed(I1.class.getField("f1"))); + assertTrue(e2Policy.isFieldExposed(I1.class.getField("f1"))); + } + } + + @Test + public void testArrayArgs() throws NoSuchMethodException { + { + WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy( + CArrayArgs.class.getName() + ".m1(java.lang.String)", + CArrayArgs.class.getName() + ".m1(java.lang.String[])", + CArrayArgs.class.getName() + ".m1(java.lang.String[][])", + CArrayArgs.class.getName() + ".m2(" + C1.class.getName() + "[])", + CArrayArgs.class.getName() + ".m2(" + + C1.class.getName() + "[], " + + C1.class.getName() + "[], " + + C1.class.getName() + ")" + ); + ClassMemberAccessPolicy classPolicy = policy.forClass(CArrayArgs.class); + assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String.class))); + assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String[].class))); + assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String[][].class))); + assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m2", C1[].class))); + assertTrue(classPolicy.isMethodExposed( + CArrayArgs.class.getMethod("m2", C1[].class, C1[].class, C1.class))); + } + { + WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy( + CArrayArgs.class.getName() + ".m1(java.lang.String)", + CArrayArgs.class.getName() + ".m1(java.lang.String[][])" + ); + ClassMemberAccessPolicy classPolicy = policy.forClass(CArrayArgs.class); + assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String.class))); + assertFalse(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String[].class))); + assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String[][].class))); + assertFalse(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m2", C1[].class))); + assertFalse(classPolicy.isMethodExposed( + CArrayArgs.class.getMethod("m2", C1[].class, C1[].class, C1.class))); + } + } + + @Test + public void memberSelectorParserIgnoresWhitespace() throws NoSuchMethodException { + WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy( + (CArrayArgs.class.getName() + ".m1(java.lang.String)").replace(".", "\n\t. "), + CArrayArgs.class.getName() + ".m2(" + + C1.class.getName() + " [ ]\t," + + C1.class.getName() + "[] ,\n " + + C1.class.getName() + " )" + ); + ClassMemberAccessPolicy classPolicy = policy.forClass(CArrayArgs.class); + assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String.class))); + assertTrue(classPolicy.isMethodExposed( + CArrayArgs.class.getMethod("m2", C1[].class, C1[].class, C1.class))); + } + + @Test + public void memberSelectorParsingErrorsTest() { + try { + newWhitelistMemberAccessPolicy("foo()"); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("missing dot")); + } + try { + newWhitelistMemberAccessPolicy("com.example.Foo-bar.m()"); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("malformed upper bound class name")); + } + try { + newWhitelistMemberAccessPolicy("java.util.Date.m-x()"); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("malformed member name")); + } + try { + newWhitelistMemberAccessPolicy("java.util.Date.to string()"); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("malformed member name")); + } + try { + newWhitelistMemberAccessPolicy("java.util.Date.toString("); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("missing closing ')'")); + } + try { + newWhitelistMemberAccessPolicy("java.util.Date.m(com.x-y)"); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("malformed argument class name")); + } + try { + newWhitelistMemberAccessPolicy("java.util.Date.m(int[)"); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("malformed argument class name")); + } + } + + @Test + public void testAnnotation() throws NoSuchFieldException, NoSuchMethodException { + WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy( + CAnnotationsTest2.class.getName() + ".f2", + CAnnotationsTest2.class.getName() + ".f3", + CAnnotationsTest2.class.getName() + ".m2()", + CAnnotationsTest2.class.getName() + ".m3()", + CAnnotationsTest2.class.getName() + "." + CAnnotationsTest2.class.getSimpleName() + "(int)", + CAnnotationsTest2.class.getName() + "." + CAnnotationsTest2.class.getSimpleName() + "(int, int)" + ); + ClassMemberAccessPolicy classPolicy = policy.forClass(CAnnotationsTest2.class); + + assertFalse(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f1"))); + assertTrue(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f2"))); + assertTrue(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f3"))); + assertTrue(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f4"))); + assertTrue(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f5"))); + assertTrue(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f6"))); + + assertFalse(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m1"))); + assertTrue(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m2"))); + assertTrue(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m3"))); + assertTrue(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m4"))); + assertTrue(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m5"))); + assertTrue(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m6"))); + + assertTrue(classPolicy.isConstructorExposed( + CAnnotationsTest2.class.getConstructor())); + assertTrue(classPolicy.isConstructorExposed( + CAnnotationsTest2.class.getConstructor(int.class))); + assertTrue(classPolicy.isConstructorExposed( + CAnnotationsTest2.class.getConstructor(int.class, int.class))); + assertTrue(classPolicy.isConstructorExposed( + CAnnotationsTest2.class.getConstructor(int.class, int.class, int.class))); + assertFalse(classPolicy.isConstructorExposed( + CAnnotationsTest2.class.getConstructor(int.class, int.class, int.class, int.class))); + } + + public static final MemberAccessPolicy CONFIG_TEST_MEMBER_ACCESS_POLICY = + newWhitelistMemberAccessPolicy( + C1.class.getName() + ".m1()", + C1.class.getName() + ".m3()"); + + @Test + public void stringBasedConfigurationTest() throws TemplateException { + Configuration cfg = new Configuration(Configuration.VERSION_2_3_30); + cfg.setSetting( + "objectWrapper", + "DefaultObjectWrapper(2.3.30, " + + "memberAccessPolicy=" + + WhitelistMemberAccessPolicyTest.class.getName() + ".CONFIG_TEST_MEMBER_ACCESS_POLICY" + + ")"); + TemplateHashModel m = (TemplateHashModel) cfg.getObjectWrapper().wrap(new C1()); + assertNotNull(m.get("m1")); + assertNull(m.get("m2")); + assertNotNull(m.get("m3")); + } + + private static WhitelistMemberAccessPolicy newWhitelistMemberAccessPolicy(String... memberSelectors) { + return new WhitelistMemberAccessPolicy( + WhitelistMemberAccessPolicy.MemberSelector.parse( + Arrays.asList(memberSelectors), + WhitelistMemberAccessPolicyTest.class.getClassLoader())); + } + + public static class C1 { + public int f1; + public int f2; + + public C1() { + } + + public C1(int x) { + } + + public void m1() { + } + + public void m2() { + } + + public void m2(int x) { + } + + public void m2(double x) { + } + + public void m3() { + } + } + + public static class C2 extends C1 { + public int f3; + + public C2(int x) { + super(x); + } + + public void m2(boolean x) { + } + + public void m4() { + } + } + + public static class C3 extends C2 { + public C3() { + super(0); + } + + public C3(int x) { + super(x); + } + } + + public static class D1 implements I1 { + public int f1; + public void m1() { + } + } + + public static class D2 extends D1 { + } + + public static class E1 implements I1Sub { + public void m1() { + + } + + public void m2() { + } + } + + public static class E2 extends E1 implements J1 { + } + + public static class F1 implements J1 { + public void m2() { + } + } + + interface I1 { + int f1 = 1; + void m1(); + } + + interface I1Sub extends Serializable, I1 { + void m2(); + } + + interface J1 { + void m2(); + } + + public class CArrayArgs { + public void m1(String arg) { + } + + public void m1(String[] arg) { + } + + public void m1(String[][] arg) { + } + + public void m2(C1[] arg) { + } + + public void m2(C1[] arg1, C1[] arg2, C1 arg3) { + } + } + + public static class CAnnotationsTest1 { + @TemplateAccessible + public int f5; + + @TemplateAccessible + public CAnnotationsTest1() {} + + @TemplateAccessible + public void m5() {} + } + + public interface IAnnotationTest { + @TemplateAccessible + int f6 = 0; + + @TemplateAccessible + void m6(); + } + + public static class CAnnotationsTest2 extends CAnnotationsTest1 implements IAnnotationTest { + public int f1; + + public int f2; + + @TemplateAccessible + public int f3; + + @TemplateAccessible + public int f4; + + public int f5; + + public int f6; + + public CAnnotationsTest2() {} + + public CAnnotationsTest2(int x) {} + + @TemplateAccessible + public CAnnotationsTest2(int x, int y) {} + + @TemplateAccessible + public CAnnotationsTest2(int x, int y, int z) {} + + public CAnnotationsTest2(int x, int y, int z, int a) {} + + public void m1() {} + + public void m2() {} + + @TemplateAccessible + public void m3() {} + + @TemplateAccessible + public void m4() {} + + public void m5() {} + + public void m6() {} + } + +} diff --git a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java index 86d14bb..12f4954 100644 --- a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java +++ b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -56,6 +57,7 @@ import com.google.common.collect.ImmutableMap; import freemarker.ext.beans.BeansWrapper; import freemarker.ext.beans.EnumerationModel; import freemarker.ext.beans.HashAdapter; +import freemarker.ext.beans.WhitelistMemberAccessPolicy; import freemarker.ext.util.WrapperTemplateModel; public class DefaultObjectWrapperTest { @@ -316,6 +318,30 @@ public class DefaultObjectWrapperTest { assertTrue(bw.wrap(new PureIterable()) instanceof DefaultIterableAdapter); } + + { + DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.getVersion()); + + DefaultObjectWrapper bwDefault = builder.build(); + assertSame(bwDefault, builder.build()); + + WhitelistMemberAccessPolicy memberAccessPolicy = + new WhitelistMemberAccessPolicy( + WhitelistMemberAccessPolicy.MemberSelector.parse( + Arrays.asList(SomeBean.class.getName() + ".getX()"), + DefaultObjectWrapperTest.class.getClassLoader())); + builder.setMemberAccessPolicy(memberAccessPolicy); + DefaultObjectWrapper bw = builder.build(); + assertNotSame(bw, bwDefault); + assertSame(bw, builder.build()); + assertSame(bw.getMemberAccessPolicy(), memberAccessPolicy); + + TemplateHashModel m = (TemplateHashModel) bw.wrap(new SomeBean()); + assertNotNull(m.get("x")); + assertNotNull(m.get("getX")); + assertNull(m.get("y")); + assertNull(m.get("getY")); + } } @Test @@ -1191,5 +1217,14 @@ public class DefaultObjectWrapperTest { } }; + + public static class SomeBean { + public int getX() { + return 1; + } + public int getY() { + return 1; + } + } }
