Copilot commented on code in PR #15808:
URL: https://github.com/apache/grails-core/pull/15808#discussion_r3509505533


##########
grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java:
##########
@@ -216,15 +236,905 @@ public static BindingResult bindObjectToInstance(Object 
object, Object source, L
             include = getBindingIncludeList(object);
         }
         GrailsApplication application = Holders.findApplication();
-        PersistentEntity entity = null;
+        PersistentEntity entity = findPersistentEntity(application, object);
+        return bindObjectToDomainInstance(entity, object, source, include, 
exclude, filter);
+    }
+
+    private static PersistentEntity findPersistentEntity(GrailsApplication 
application, Object object) {
         if (application != null) {
             try {
-                entity = 
application.getMappingContext().getPersistentEntity(object.getClass().getName());
+                return 
application.getMappingContext().getPersistentEntity(object.getClass().getName());
             } catch (GrailsConfigurationException e) {
-                //no-op
+                return null;
             }
         }
-        return bindObjectToDomainInstance(entity, object, source, include, 
exclude, filter);
+        return null;
+    }
+
+    public static DataBindingSource createSecureDataBindingSource(Object 
object, Object source, List allowedParams, String filter) {
+        GrailsApplication application = Holders.findApplication();
+        DataBindingSource bindingSource = createDataBindingSource(application, 
object.getClass(), source);
+        return createSecureDataBindingSource(object, bindingSource, 
allowedParams, filter);
+    }
+
+    public static BindingResult secureBindObjectToInstance(Object object, 
Object source, List allowedParams, String filter, boolean nullMissing) {
+        BindingResult bindingResult = null;
+        GrailsApplication grailsApplication = Holders.findApplication();
+        PersistentEntity entity = findPersistentEntity(grailsApplication, 
object);
+
+        try {
+            final DataBindingSource bindingSource = 
createDataBindingSource(grailsApplication, object.getClass(), source);
+            final DataBindingSource secureBindingSource = 
createSecureDataBindingSource(object, bindingSource, allowedParams, filter);
+            final DataBinder grailsWebDataBinder = 
getGrailsWebDataBinder(grailsApplication);
+            grailsWebDataBinder.bind(object, secureBindingSource, null, 
allowedParams, Collections.emptyList());
+            if (nullMissing) {
+                assignNullToMissingAllowedProperties(object, 
secureBindingSource, allowedParams);
+            }
+        } catch (InvalidRequestBodyException e) {
+            String messageCode = "invalidRequestBody";
+            Class objectType = object.getClass();
+            String defaultMessage = "An error occurred parsing the body of the 
request";
+            String[] codes = getMessageCodes(messageCode, objectType);
+            bindingResult = new BeanPropertyBindingResult(object, 
objectType.getName());
+            bindingResult.addError(new 
ObjectError(bindingResult.getObjectName(), codes, null, defaultMessage));
+        } catch (Exception e) {
+            bindingResult = new BeanPropertyBindingResult(object, 
object.getClass().getName());
+            bindingResult.addError(new 
ObjectError(bindingResult.getObjectName(), e.getMessage()));
+        }

Review Comment:
   In the generic exception handler, ObjectError is created with e.getMessage() 
as the default message. e.getMessage() can be null, which can lead to an 
empty/invalid error message (and may break message resolution depending on 
Spring internals). Provide a non-null fallback message (optionally including 
the exception class) to keep binding errors actionable.



##########
grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java:
##########
@@ -263,6 +1173,10 @@ public static BindingResult 
bindObjectToDomainInstance(PersistentEntity entity,
             bindingResult.addError(new 
ObjectError(bindingResult.getObjectName(), e.getMessage()));

Review Comment:
   ObjectError defaultMessage is taken from e.getMessage(), which can be null 
and results in an unhelpful/empty binding error. Use a non-null fallback (for 
example, include the exception class name) so consumers and tests get a 
reliable error message.



##########
grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java:
##########
@@ -216,15 +236,905 @@ public static BindingResult bindObjectToInstance(Object 
object, Object source, L
             include = getBindingIncludeList(object);
         }
         GrailsApplication application = Holders.findApplication();
-        PersistentEntity entity = null;
+        PersistentEntity entity = findPersistentEntity(application, object);
+        return bindObjectToDomainInstance(entity, object, source, include, 
exclude, filter);
+    }
+
+    private static PersistentEntity findPersistentEntity(GrailsApplication 
application, Object object) {
         if (application != null) {
             try {
-                entity = 
application.getMappingContext().getPersistentEntity(object.getClass().getName());
+                return 
application.getMappingContext().getPersistentEntity(object.getClass().getName());
             } catch (GrailsConfigurationException e) {
-                //no-op
+                return null;
             }
         }
-        return bindObjectToDomainInstance(entity, object, source, include, 
exclude, filter);
+        return null;
+    }
+
+    public static DataBindingSource createSecureDataBindingSource(Object 
object, Object source, List allowedParams, String filter) {
+        GrailsApplication application = Holders.findApplication();
+        DataBindingSource bindingSource = createDataBindingSource(application, 
object.getClass(), source);
+        return createSecureDataBindingSource(object, bindingSource, 
allowedParams, filter);
+    }
+
+    public static BindingResult secureBindObjectToInstance(Object object, 
Object source, List allowedParams, String filter, boolean nullMissing) {
+        BindingResult bindingResult = null;
+        GrailsApplication grailsApplication = Holders.findApplication();
+        PersistentEntity entity = findPersistentEntity(grailsApplication, 
object);
+
+        try {
+            final DataBindingSource bindingSource = 
createDataBindingSource(grailsApplication, object.getClass(), source);
+            final DataBindingSource secureBindingSource = 
createSecureDataBindingSource(object, bindingSource, allowedParams, filter);
+            final DataBinder grailsWebDataBinder = 
getGrailsWebDataBinder(grailsApplication);
+            grailsWebDataBinder.bind(object, secureBindingSource, null, 
allowedParams, Collections.emptyList());
+            if (nullMissing) {
+                assignNullToMissingAllowedProperties(object, 
secureBindingSource, allowedParams);
+            }
+        } catch (InvalidRequestBodyException e) {
+            String messageCode = "invalidRequestBody";
+            Class objectType = object.getClass();
+            String defaultMessage = "An error occurred parsing the body of the 
request";
+            String[] codes = getMessageCodes(messageCode, objectType);
+            bindingResult = new BeanPropertyBindingResult(object, 
objectType.getName());
+            bindingResult.addError(new 
ObjectError(bindingResult.getObjectName(), codes, null, defaultMessage));
+        } catch (Exception e) {
+            bindingResult = new BeanPropertyBindingResult(object, 
object.getClass().getName());
+            bindingResult.addError(new 
ObjectError(bindingResult.getObjectName(), e.getMessage()));
+        }
+
+        return processBindingResult(entity, object, bindingResult);
+    }
+
+    public static DataBindingSource 
createSecureDataBindingSource(DataBindingSource bindingSource, List 
allowedParams, String filter) {
+        return createSecureDataBindingSource(null, bindingSource, 
allowedParams, filter);
+    }
+
+    private static DataBindingSource createSecureDataBindingSource(Object 
object, DataBindingSource bindingSource, List allowedParams, String filter) {
+        Map secureSource = new LinkedHashMap();
+        for (Object allowedParam : allowedParams) {
+            if (allowedParam instanceof CharSequence) {
+                String propertyName = allowedParam.toString();
+                String sourcePropertyName = filter == null ? propertyName : 
filter + "." + propertyName;
+                copyAllowedProperty(secureSource, propertyName, propertyName, 
object, object == null ? null : object.getClass(), bindingSource, 
sourcePropertyName);
+                copyAllowedCheckboxMarker(secureSource, propertyName, 
bindingSource, sourcePropertyName);
+            }
+        }
+        return new SimpleMapDataBindingSource(secureSource);
+    }
+
+    private static void copyAllowedCheckboxMarker(Map secureSource, String 
targetPropertyName, Object source, String sourcePropertyName) {
+        String targetMarkerPropertyName = 
checkboxMarkerPropertyName(targetPropertyName);
+        String sourceMarkerPropertyName = 
checkboxMarkerPropertyName(sourcePropertyName);
+        copyAllowedProperty(secureSource, targetMarkerPropertyName, source, 
sourceMarkerPropertyName);
+    }
+
+    private static String checkboxMarkerPropertyName(String propertyName) {
+        int separator = lastPropertyPathSeparator(propertyName);
+        if (separator == -1) {
+            return "_" + propertyName;
+        }
+        return propertyName.substring(0, separator + 1) + "_" + 
propertyName.substring(separator + 1);
+    }
+
+    private static boolean copyAllowedProperty(Map secureSource, String 
targetPropertyName, Object source, String sourcePropertyName) {
+        return copyAllowedProperty(secureSource, targetPropertyName, 
targetPropertyName, null, null, source, sourcePropertyName);
+    }
+
+    private static boolean copyAllowedProperty(Map secureSource, String 
targetPropertyName, String targetLookupPropertyName, Object target, Class 
targetType, Object source, String sourcePropertyName) {
+        if (containsSourceProperty(source, sourcePropertyName)) {
+            if (shouldCopyExactSourceProperty(target, targetType, 
targetLookupPropertyName, sourcePropertyName)) {
+                putNestedValue(secureSource, targetPropertyName, 
getSourcePropertyValue(source, sourcePropertyName));
+                return true;
+            }
+        }
+        int separator = propertyPathSeparator(sourcePropertyName);
+        if (separator == -1) {
+            return false;
+        }
+        if (copyAllowedIndexedPathProperty(secureSource, targetPropertyName, 
source, sourcePropertyName)) {
+            return true;
+        }
+        if (copyAllowedIndexedRootProperty(secureSource, targetPropertyName, 
source, sourcePropertyName)) {
+            return true;
+        }
+        String sourceRootPropertyName = sourcePropertyName.substring(0, 
separator);
+        String nestedSourcePropertyName = 
sourcePropertyName.substring(separator + 1);
+        if (!containsSourceProperty(source, sourceRootPropertyName)) {
+            return copyAllowedIndexedProperty(secureSource, 
targetPropertyName, source, sourceRootPropertyName, nestedSourcePropertyName);
+        }
+        Object nestedSource = getSourcePropertyValue(source, 
sourceRootPropertyName);
+        String nestedTargetLookupPropertyName = targetLookupPropertyName;
+        Object nestedTarget = target;
+        Class nestedTargetType = targetType;
+        Class collectionElementType = null;
+        Class mapValueType = null;
+        boolean expandMapEntries = false;
+        if (splitPropertyPath(sourcePropertyName).length <= 
splitPropertyPath(targetLookupPropertyName).length) {
+            int targetSeparator = 
propertyPathSeparator(targetLookupPropertyName);
+            if (targetSeparator == -1) {
+                return false;
+            }
+            String targetRootPropertyName = 
targetLookupPropertyName.substring(0, targetSeparator);
+            nestedTargetLookupPropertyName = 
targetLookupPropertyName.substring(targetSeparator + 1);
+            nestedTargetType = getTargetPropertyType(target, targetType, 
targetRootPropertyName);
+            collectionElementType = getCollectionElementType(target, 
targetType, targetRootPropertyName);
+            mapValueType = getMapValueType(target, targetType, 
targetRootPropertyName);
+            expandMapEntries = shouldExpandMapEntries(target, targetType, 
targetRootPropertyName);
+            nestedTarget = getTargetPropertyValue(target, 
targetRootPropertyName);
+        }
+        if (nestedSource instanceof Collection) {
+            return copyAllowedCollectionProperty(secureSource, 
targetPropertyName, (Collection) nestedSource, nestedSourcePropertyName, 
nestedTargetLookupPropertyName, nestedTarget, collectionElementType);
+        }
+        if (nestedSource instanceof Map) {
+            if (expandMapEntries) {
+                return copyAllowedMapProperty(secureSource, 
targetPropertyName, (Map) nestedSource, nestedSourcePropertyName, mapValueType);
+            }
+            if (copyAllowedDirectScalarMapProperty(secureSource, 
targetPropertyName, (Map) nestedSource, nestedSourcePropertyName)) {
+                return true;
+            }
+            if (copyAllowedDirectMapProperty(secureSource, targetPropertyName, 
(Map) nestedSource, nestedSourcePropertyName)) {
+                return true;
+            }
+        }
+        return copyAllowedProperty(secureSource, targetPropertyName, 
nestedTargetLookupPropertyName, nestedTarget, nestedTargetType, nestedSource, 
nestedSourcePropertyName);
+    }
+
+    private static boolean shouldCopyExactSourceProperty(Object target, Class 
targetType, String targetLookupPropertyName, String sourcePropertyName) {
+        if (propertyPathSeparator(sourcePropertyName) == -1) {
+            return true;
+        }
+        int targetSeparator = propertyPathSeparator(targetLookupPropertyName);
+        if (targetSeparator == -1) {
+            return true;
+        }
+        String targetRootPropertyName = targetLookupPropertyName.substring(0, 
targetSeparator);
+        return !shouldExpandMapEntries(target, targetType, 
targetRootPropertyName);
+    }
+
+    private static boolean shouldExpandMapEntries(Object target, Class 
targetType, String propertyName) {
+        Object value = getTargetPropertyValue(target, propertyName);
+        if (value instanceof Map && hasStructuredTargetMapValues((Map) value)) 
{
+            return true;
+        }
+
+        Class mapValueType = getMapValueType(target, targetType, propertyName);
+        return mapValueType != null && isStructuredMapValueType(mapValueType);
+    }
+
+    private static boolean hasStructuredTargetMapValues(Map map) {
+        for (Object value : map.values()) {
+            if (value != null && isStructuredMapValueType(value.getClass())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean isStructuredMapValueType(Class valueType) {
+        Package valuePackage = valueType.getPackage();
+        return !valueType.isPrimitive()
+            && (valuePackage == null || 
!valuePackage.getName().startsWith("java."))
+            && !CharSequence.class.isAssignableFrom(valueType)
+            && !Number.class.isAssignableFrom(valueType)
+            && !Boolean.class.isAssignableFrom(valueType)
+            && !Enum.class.isAssignableFrom(valueType)
+            && !Map.class.isAssignableFrom(valueType)
+            && !Collection.class.isAssignableFrom(valueType)
+            && !Object.class.equals(valueType);
+    }
+
+    private static Class getMapValueType(Object target, Class targetType, 
String propertyName) {
+        Class resolvedTargetType = getTargetType(target, targetType);
+        if (resolvedTargetType == null) {
+            return null;
+        }
+
+        MetaClass mc = 
GroovySystem.getMetaClassRegistry().getMetaClass(resolvedTargetType);
+        MetaProperty metaProperty = mc.getMetaProperty(propertyName);
+        if (metaProperty == null || 
!Map.class.isAssignableFrom(metaProperty.getType())) {
+            return null;
+        }
+
+        Field field = findField(resolvedTargetType, propertyName);
+        if (field == null) {
+            return null;
+        }
+        return getMapValueType(field.getGenericType());
+    }
+
+    private static Class getCollectionElementType(Object target, Class 
targetType, String propertyName) {
+        Object value = getTargetPropertyValue(target, propertyName);
+        if (value instanceof Collection && !((Collection) value).isEmpty()) {
+            Object firstValue = ((Collection) value).iterator().next();
+            if (firstValue != null) {
+                return firstValue.getClass();
+            }
+        }
+
+        Class resolvedTargetType = getTargetType(target, targetType);
+        if (resolvedTargetType == null) {
+            return null;
+        }
+        Field field = findField(resolvedTargetType, propertyName);
+        if (field == null) {
+            return null;
+        }
+        return getCollectionElementType(field.getGenericType());
+    }
+
+    private static Class getCollectionElementType(Type type) {
+        if (!(type instanceof ParameterizedType)) {
+            return null;
+        }
+
+        Type[] typeArguments = ((ParameterizedType) 
type).getActualTypeArguments();
+        if (typeArguments.length == 0) {
+            return null;
+        }
+        Type valueType = typeArguments[0];
+        if (valueType instanceof Class) {
+            return (Class) valueType;
+        }
+        if (valueType instanceof ParameterizedType && ((ParameterizedType) 
valueType).getRawType() instanceof Class) {
+            return (Class) ((ParameterizedType) valueType).getRawType();
+        }
+        return null;
+    }
+
+    private static Class getTargetPropertyType(Object target, Class 
targetType, String propertyName) {
+        Object value = getTargetPropertyValue(target, propertyName);
+        if (value != null) {
+            return value.getClass();
+        }
+
+        Class resolvedTargetType = getTargetType(target, targetType);
+        if (resolvedTargetType == null) {
+            return null;
+        }
+        MetaClass mc = 
GroovySystem.getMetaClassRegistry().getMetaClass(resolvedTargetType);
+        MetaProperty metaProperty = mc.getMetaProperty(propertyName);
+        if (metaProperty != null) {
+            return metaProperty.getType();
+        }
+        Field field = findField(resolvedTargetType, propertyName);
+        return field == null ? null : field.getType();
+    }
+
+    private static Class getTargetType(Object target, Class targetType) {
+        if (target != null) {
+            return target.getClass();
+        }
+        return targetType;
+    }
+
+    private static Class getMapValueType(Type type) {
+        if (!(type instanceof ParameterizedType)) {
+            return null;
+        }
+
+        Type[] typeArguments = ((ParameterizedType) 
type).getActualTypeArguments();
+        if (typeArguments.length < 2) {
+            return null;
+        }
+        Type valueType = typeArguments[1];
+        if (valueType instanceof Class) {
+            return (Class) valueType;
+        }
+        if (valueType instanceof ParameterizedType && ((ParameterizedType) 
valueType).getRawType() instanceof Class) {
+            return (Class) ((ParameterizedType) valueType).getRawType();
+        }
+        return null;
+    }
+
+    private static Object getTargetPropertyValue(Object target, String 
propertyName) {
+        if (target == null) {
+            return null;
+        }
+
+        try {
+            MetaClass mc = 
GroovySystem.getMetaClassRegistry().getMetaClass(target.getClass());
+            return mc.getProperty(target, propertyName);
+        }
+        catch (Exception e) {
+            return null;
+        }
+    }
+
+    private static Field findField(Class type, String propertyName) {
+        Class currentType = type;
+        while (currentType != null) {
+            try {
+                return currentType.getDeclaredField(propertyName);
+            }
+            catch (NoSuchFieldException e) {
+                currentType = currentType.getSuperclass();
+            }
+        }
+        return null;
+    }
+
+    private static boolean copyAllowedIndexedProperty(Map secureSource, String 
targetPropertyName, Object source, String sourceRootPropertyName, String 
nestedSourcePropertyName) {
+        int separator = propertyPathSeparator(targetPropertyName);
+        if (separator == -1) {
+            return false;
+        }
+
+        String targetRootPropertyName = targetPropertyName.substring(0, 
separator);
+        if (!sourceRootPropertyName.equals(targetRootPropertyName)) {
+            return false;
+        }
+        String nestedTargetPropertyName = 
targetPropertyName.substring(separator + 1);
+        boolean copied = false;
+        String indexedSourcePropertyPrefix = sourceRootPropertyName + "[";
+        for (String indexedSourcePropertyName : 
getIndexedSourcePropertyNames(source, indexedSourcePropertyPrefix)) {
+            String targetIndexedPropertyName = targetRootPropertyName + 
indexedSourcePropertyName.substring(sourceRootPropertyName.length());
+            String targetIndexedNestedPropertyName = targetIndexedPropertyName 
+ "." + nestedTargetPropertyName;
+            if (containsSourceProperty(source, indexedSourcePropertyName)) {
+                Object nestedSource = getSourcePropertyValue(source, 
indexedSourcePropertyName);
+                if (copyAllowedProperty(secureSource, 
targetIndexedNestedPropertyName, nestedSource, nestedSourcePropertyName)) {
+                    copied = true;
+                }
+            }
+            else if (copyAllowedProperty(secureSource, 
targetIndexedNestedPropertyName, source, indexedSourcePropertyName + "." + 
nestedSourcePropertyName)) {
+                copied = true;
+            }
+        }
+        return copied;
+    }
+
+    private static boolean copyAllowedIndexedPathProperty(Map secureSource, 
String targetPropertyName, Object source, String sourcePropertyName) {
+        boolean copied = false;
+        String[] targetSegments = splitPropertyPath(targetPropertyName);
+        String[] sourceSegments = splitPropertyPath(sourcePropertyName);
+        int exactPrefixSegments = sourceSegments.length - 
targetSegments.length;
+        for (String indexedSourcePropertyName : 
getSourcePropertyNames(source)) {
+            if (indexedPropertyPathMatches(indexedSourcePropertyName, 
sourcePropertyName, exactPrefixSegments)) {
+                putNestedValue(secureSource, 
indexedTargetPropertyName(targetPropertyName, sourcePropertyName, 
indexedSourcePropertyName), getSourcePropertyValue(source, 
indexedSourcePropertyName));
+                copied = true;
+            }
+        }
+        return copied;
+    }
+
+    private static boolean copyAllowedIndexedRootProperty(Map secureSource, 
String targetPropertyName, Object source, String sourcePropertyName) {
+        int sourceSeparator = lastPropertyPathSeparator(sourcePropertyName);
+        int targetSeparator = lastPropertyPathSeparator(targetPropertyName);
+        if (sourceSeparator == -1 || targetSeparator == -1) {
+            return false;
+        }
+
+        String sourceRootPropertyName = sourcePropertyName.substring(0, 
sourceSeparator);
+        String targetRootPropertyName = targetPropertyName.substring(0, 
targetSeparator);
+        String nestedSourcePropertyName = 
sourcePropertyName.substring(sourceSeparator + 1);
+        String nestedTargetPropertyName = 
targetPropertyName.substring(targetSeparator + 1);
+        String[] targetSegments = splitPropertyPath(targetRootPropertyName);
+        String[] sourceSegments = splitPropertyPath(sourceRootPropertyName);
+        int exactPrefixSegments = sourceSegments.length - 
targetSegments.length;
+        boolean copied = false;
+        for (String indexedSourcePropertyName : 
getSourcePropertyNames(source)) {
+            if (indexedPropertyPathMatches(indexedSourcePropertyName, 
sourceRootPropertyName, exactPrefixSegments) && containsSourceProperty(source, 
indexedSourcePropertyName)) {
+                String targetIndexedPropertyName = 
indexedTargetPropertyName(targetRootPropertyName, sourceRootPropertyName, 
indexedSourcePropertyName);
+                Object nestedSource = getSourcePropertyValue(source, 
indexedSourcePropertyName);
+                if (copyAllowedProperty(secureSource, 
targetIndexedPropertyName + "." + nestedTargetPropertyName, nestedSource, 
nestedSourcePropertyName)) {
+                    copied = true;
+                }
+            }
+        }
+        return copied;
+    }
+
+    private static int propertyPathSeparator(String propertyName) {
+        return propertyPathSeparator(propertyName, false);
+    }
+
+    private static int lastPropertyPathSeparator(String propertyName) {
+        return propertyPathSeparator(propertyName, true);
+    }
+
+    private static int propertyPathSeparator(String propertyName, boolean 
last) {
+        int separator = -1;
+        int bracketDepth = 0;
+        for (int i = 0; i < propertyName.length(); i++) {
+            char character = propertyName.charAt(i);
+            if (character == '[') {
+                bracketDepth++;
+            }
+            else if (character == ']' && bracketDepth > 0) {
+                bracketDepth--;
+            }
+            else if (character == '.' && bracketDepth == 0) {
+                if (!last) {
+                    return i;
+                }
+                separator = i;
+            }
+        }
+        return separator;
+    }
+
+    private static String indexedTargetPropertyName(String targetPropertyName, 
String sourcePropertyName, String indexedSourcePropertyName) {
+        String[] targetSegments = splitPropertyPath(targetPropertyName);
+        String[] sourceSegments = splitPropertyPath(sourcePropertyName);
+        String[] indexedSourceSegments = 
splitPropertyPath(indexedSourcePropertyName);
+        int sourceOffset = Math.max(0, sourceSegments.length - 
targetSegments.length);
+        int targetOffset = Math.max(0, targetSegments.length - 
sourceSegments.length);
+        StringBuilder indexedTargetPropertyName = new StringBuilder();
+        for (int i = 0; i < targetSegments.length; i++) {
+            if (i > 0) {
+                indexedTargetPropertyName.append('.');
+            }
+            String targetSegment = targetSegments[i];
+            int sourceIndex = i - targetOffset + sourceOffset;
+            if (sourceIndex < 0 || sourceIndex >= indexedSourceSegments.length 
|| sourceIndex >= sourceSegments.length) {
+                indexedTargetPropertyName.append(targetSegment);
+                continue;
+            }
+
+            String indexedSourceSegment = indexedSourceSegments[sourceIndex];
+            String sourceSegment = sourceSegments[sourceIndex];
+            if (indexedSegmentMatches(indexedSourceSegment, sourceSegment)) {
+                
indexedTargetPropertyName.append(targetSegment).append(indexedSourceSegment.substring(sourceSegment.length()));
+            }
+            else {
+                indexedTargetPropertyName.append(targetSegment);
+            }
+        }
+        return indexedTargetPropertyName.toString();
+    }
+
+    private static boolean indexedPropertyPathMatches(String 
indexedPropertyName, String propertyName, int exactPrefixSegments) {
+        String[] indexedPropertySegments = 
splitPropertyPath(indexedPropertyName);
+        String[] propertySegments = splitPropertyPath(propertyName);
+        if (indexedPropertySegments.length != propertySegments.length) {
+            return false;
+        }
+        boolean indexed = false;
+        for (int i = 0; i < propertySegments.length; i++) {
+            if (indexedPropertySegments[i].equals(propertySegments[i])) {
+                continue;
+            }
+            if (i < exactPrefixSegments) {
+                return false;
+            }
+            if (!indexedSegmentMatches(indexedPropertySegments[i], 
propertySegments[i])) {
+                return false;
+            }
+            indexed = true;
+        }
+        return indexed;
+    }
+
+    private static String[] splitPropertyPath(String propertyName) {
+        List<String> segments = new ArrayList<>();
+        StringBuilder segment = new StringBuilder();
+        int bracketDepth = 0;
+        for (int i = 0; i < propertyName.length(); i++) {
+            char character = propertyName.charAt(i);
+            if (character == '.' && bracketDepth == 0) {
+                segments.add(segment.toString());
+                segment.setLength(0);
+            }
+            else {
+                if (character == '[') {
+                    bracketDepth++;
+                }
+                else if (character == ']' && bracketDepth > 0) {
+                    bracketDepth--;
+                }
+                segment.append(character);
+            }
+        }
+        segments.add(segment.toString());
+        return segments.toArray(new String[0]);
+    }
+
+    private static boolean indexedSegmentMatches(String indexedSegment, String 
segment) {
+        return indexedSegment.startsWith(segment + "[") && 
indexedSegment.endsWith("]");
+    }
+
+    private static Set<String> getIndexedSourcePropertyNames(Object source, 
String indexedSourcePropertyPrefix) {
+        Set<String> indexedSourcePropertyNames = new LinkedHashSet<>();
+        for (String propertyName : getSourcePropertyNames(source)) {
+            if (propertyName.startsWith(indexedSourcePropertyPrefix)) {
+                int closingIndex = propertyName.indexOf(']', 
indexedSourcePropertyPrefix.length());
+                if (closingIndex > -1) {
+                    indexedSourcePropertyNames.add(propertyName.substring(0, 
closingIndex + 1));
+                }
+            }
+        }
+        return indexedSourcePropertyNames;
+    }
+
+    private static boolean copyAllowedCollectionProperty(Map secureSource, 
String targetPropertyName, Collection collection, String sourcePropertyName) {
+        return copyAllowedCollectionProperty(secureSource, targetPropertyName, 
collection, sourcePropertyName, sourcePropertyName, null, null);
+    }
+
+    private static boolean copyAllowedCollectionProperty(Map secureSource, 
String targetPropertyName, Collection collection, String sourcePropertyName, 
String targetLookupPropertyName, Object targetCollection, Class 
targetElementType) {
+        int separator = propertyPathSeparator(targetPropertyName);
+        if (separator == -1) {
+            return false;
+        }
+        String targetRootPropertyName = targetPropertyName.substring(0, 
separator);
+        String nestedTargetPropertyName = 
targetPropertyName.substring(separator + 1);
+        List filteredCollection = getOrCreateNestedCollection(secureSource, 
targetRootPropertyName, collection.size());
+        int index = 0;
+        boolean copied = false;
+        for (Object item : collection) {
+            Map filteredItem = (Map) filteredCollection.get(index);
+            Object targetItem = getCollectionValue(targetCollection, index);
+            Class itemTargetType = targetItem == null ? targetElementType : 
targetItem.getClass();
+            if (copyAllowedProperty(filteredItem, nestedTargetPropertyName, 
targetLookupPropertyName, targetItem, itemTargetType, item, 
sourcePropertyName)) {
+                copied = true;
+            }
+            index++;
+        }
+        return copied;
+    }
+
+    private static Object getCollectionValue(Object targetCollection, int 
index) {
+        if (targetCollection instanceof List && ((List) 
targetCollection).size() > index) {
+            return ((List) targetCollection).get(index);
+        }
+        if (targetCollection instanceof Collection) {
+            int currentIndex = 0;
+            for (Object value : (Collection) targetCollection) {
+                if (currentIndex == index) {
+                    return value;
+                }
+                currentIndex++;
+            }
+        }
+        return null;
+    }
+
+    private static boolean copyAllowedDirectScalarMapProperty(Map 
secureSource, String targetPropertyName, Map map, String sourcePropertyName) {
+        if (propertyPathSeparator(sourcePropertyName) > -1 || 
!containsSourceProperty(map, sourcePropertyName)) {
+            return false;
+        }
+        Object directValue = getSourcePropertyValue(map, sourcePropertyName);
+        if (isNestedSource(directValue)) {
+            return false;
+        }
+        putNestedValue(secureSource, targetPropertyName, directValue);
+        return true;
+    }
+
+    private static boolean copyAllowedDirectMapProperty(Map secureSource, 
String targetPropertyName, Map map, String sourcePropertyName) {
+        if (propertyPathSeparator(sourcePropertyName) > -1 || 
!containsSourceProperty(map, sourcePropertyName)) {
+            return false;
+        }
+        putNestedValue(secureSource, targetPropertyName, 
getSourcePropertyValue(map, sourcePropertyName));
+        return true;
+    }
+
+    private static boolean copyAllowedMapProperty(Map secureSource, String 
targetPropertyName, Map map, String sourcePropertyName, Class targetValueType) {
+        if (!hasNestedSourceEntries(map)) {
+            return false;
+        }
+
+        int separator = propertyPathSeparator(targetPropertyName);
+        if (separator == -1) {
+            return false;
+        }
+        String targetRootPropertyName = targetPropertyName.substring(0, 
separator);
+        String nestedTargetPropertyName = 
targetPropertyName.substring(separator + 1);
+        boolean copied = false;
+        for (Object entryObject : map.entrySet()) {
+            Map.Entry entry = (Map.Entry) entryObject;
+            String targetIndexedPropertyName = targetRootPropertyName + "[" + 
entry.getKey() + "]";
+            if (copyAllowedProperty(secureSource, targetIndexedPropertyName + 
"." + nestedTargetPropertyName, nestedTargetPropertyName, null, 
targetValueType, entry.getValue(), sourcePropertyName)) {
+                copied = true;
+            }
+        }
+        return copied;
+    }
+
+    private static boolean isNestedSource(Object value) {
+        return value instanceof Map || value instanceof Collection || value 
instanceof DataBindingSource;
+    }
+
+    private static boolean hasNestedSourceEntries(Map map) {
+        for (Object value : map.values()) {
+            if (isNestedSource(value)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static List getOrCreateNestedCollection(Map secureSource, String 
propertyName, int size) {
+        Object existingValue = secureSource.get(propertyName);
+        List collection;
+        if (existingValue instanceof List) {
+            collection = (List) existingValue;
+        }
+        else {
+            collection = new ArrayList(size);
+            secureSource.put(propertyName, collection);
+        }
+        while (collection.size() < size) {
+            collection.add(new LinkedHashMap());
+        }
+        return collection;
+    }
+
+    private static void putNestedValue(Map secureSource, String propertyName, 
Object value) {
+        int separator = propertyPathSeparator(propertyName);
+        if (separator == -1) {
+            secureSource.put(propertyName, value);
+            return;
+        }
+        String rootPropertyName = propertyName.substring(0, separator);
+        Map nestedSource = getOrCreateNestedMap(secureSource, 
rootPropertyName);
+        putNestedValue(nestedSource, propertyName.substring(separator + 1), 
value);
+    }
+
+    private static Map getOrCreateNestedMap(Map secureSource, String 
propertyName) {
+        Object existingValue = secureSource.get(propertyName);
+        if (existingValue instanceof Map) {
+            return (Map) existingValue;
+        }
+        Map nestedSource = new LinkedHashMap();
+        secureSource.put(propertyName, nestedSource);
+        return nestedSource;
+    }
+
+    private static boolean containsSourceProperty(Object source, String 
propertyName) {
+        if (source instanceof DataBindingSource) {
+            return ((DataBindingSource) source).containsProperty(propertyName);
+        }
+        if (source instanceof Map) {
+            return ((Map) source).containsKey(propertyName);
+        }
+        return false;
+    }
+
+    private static Set<String> getSourcePropertyNames(Object source) {
+        Set<String> propertyNames = new LinkedHashSet<>();
+        if (source instanceof DataBindingSource) {
+            propertyNames.addAll(((DataBindingSource) 
source).getPropertyNames());
+        }
+        else if (source instanceof Map) {
+            for (Object key : ((Map) source).keySet()) {
+                propertyNames.add(key.toString());
+            }
+        }
+        return propertyNames;
+    }
+
+    private static Object getSourcePropertyValue(Object source, String 
propertyName) {
+        if (source instanceof DataBindingSource) {
+            return ((DataBindingSource) source).getPropertyValue(propertyName);
+        }
+        return ((Map) source).get(propertyName);
+    }
+
+    public static void assignNullToMissingAllowedProperties(Object object, 
Object source, List allowedParams) {
+        assignNullToMissingAllowedProperties(object, source, allowedParams, 
null);
+    }
+
+    public static void assignNullToMissingAllowedProperties(Object object, 
Object source, List allowedParams, String filter) {
+        GrailsApplication application = Holders.findApplication();
+        DataBindingSource bindingSource = createDataBindingSource(application, 
object.getClass(), source);
+        assignNullToMissingAllowedProperties(object, bindingSource, 
allowedParams, filter);
+    }
+
+    private static void assignNullToMissingAllowedProperties(Object object, 
DataBindingSource bindingSource, List allowedParams, String filter) {
+        for (Object allowedParam : allowedParams) {
+            if (allowedParam instanceof CharSequence) {
+                String propertyName = allowedParam.toString();
+                if (propertyName.indexOf('*') == -1) {
+                    if (assignNullToMissingIndexedProperties(object, 
bindingSource, propertyName, filter)) {
+                        continue;
+                    }
+                    if (!bindingSourceContainsProperty(bindingSource, 
propertyName, filter)) {
+                        setPropertyToNull(object, propertyName);
+                    }
+                }
+            }
+        }
+    }
+
+    private static boolean assignNullToMissingIndexedProperties(Object object, 
DataBindingSource bindingSource, String propertyName, String filter) {
+        String sourcePropertyName = filter == null ? propertyName : filter + 
"." + propertyName;
+        return assignNullToMissingIndexedProperties(object, bindingSource, 
BLANK, sourcePropertyName, propertyName);
+    }
+
+    private static boolean assignNullToMissingIndexedProperties(Object object, 
Object source, String targetPathPrefix, String sourcePropertyName, String 
targetPropertyName) {
+        int sourceSeparator = propertyPathSeparator(sourcePropertyName);
+        int targetSeparator = propertyPathSeparator(targetPropertyName);
+        if (sourceSeparator == -1 || targetSeparator == -1) {
+            return false;
+        }
+
+        String sourceRootPropertyName = sourcePropertyName.substring(0, 
sourceSeparator);
+        String targetRootPropertyName = targetPropertyName.substring(0, 
targetSeparator);
+        String nestedSourcePropertyName = 
sourcePropertyName.substring(sourceSeparator + 1);
+        String nestedTargetPropertyName = 
targetPropertyName.substring(targetSeparator + 1);
+        if (splitPropertyPath(sourcePropertyName).length > 
splitPropertyPath(targetPropertyName).length) {
+            if (containsSourceProperty(source, sourceRootPropertyName)) {
+                return assignNullToMissingIndexedProperties(object, 
getSourcePropertyValue(source, sourceRootPropertyName), targetPathPrefix, 
nestedSourcePropertyName, targetPropertyName);
+            }
+            return false;
+        }
+
+        if (containsSourceProperty(source, sourceRootPropertyName)) {
+            Object nestedSource = getSourcePropertyValue(source, 
sourceRootPropertyName);
+            if (nestedSource instanceof Collection) {
+                return assignNullToMissingCollectionProperties(object, 
(Collection) nestedSource, targetPathPrefix, targetRootPropertyName, 
nestedSourcePropertyName, nestedTargetPropertyName);
+            }
+            Object targetObject = getTargetObject(object, targetPathPrefix);
+            if (nestedSource instanceof Map && hasNestedSourceEntries((Map) 
nestedSource) && shouldExpandMapEntries(targetObject, targetObject == null ? 
null : targetObject.getClass(), targetRootPropertyName)) {
+                return assignNullToMissingMapProperties(object, (Map) 
nestedSource, targetPathPrefix, targetRootPropertyName, 
nestedSourcePropertyName, nestedTargetPropertyName);
+            }
+        }
+
+        boolean indexed = false;
+        String indexedSourcePropertyPrefix = sourceRootPropertyName + "[";
+        for (String indexedSourcePropertyName : 
getIndexedSourcePropertyNames(source, indexedSourcePropertyPrefix)) {
+            if (containsSourceProperty(source, indexedSourcePropertyName)) {
+                indexed = true;
+                String targetIndexedPropertyName = 
appendPropertyPath(targetPathPrefix, targetRootPropertyName + 
indexedSourcePropertyName.substring(sourceRootPropertyName.length()));
+                Object nestedSource = getSourcePropertyValue(source, 
indexedSourcePropertyName);
+                if (!assignNullToMissingIndexedProperties(object, 
nestedSource, targetIndexedPropertyName, nestedSourcePropertyName, 
nestedTargetPropertyName) && !containsPropertyPath(nestedSource, 
nestedSourcePropertyName)) {
+                    setPropertyToNull(object, targetIndexedPropertyName + "." 
+ nestedTargetPropertyName);
+                }
+            }
+        }
+        return indexed;
+    }
+
+    private static boolean assignNullToMissingCollectionProperties(Object 
object, Collection collection, String targetPathPrefix, String 
targetRootPropertyName, String nestedSourcePropertyName, String 
nestedTargetPropertyName) {
+        int index = 0;
+        for (Object item : collection) {
+            String targetIndexedPropertyName = 
appendPropertyPath(targetPathPrefix, targetRootPropertyName + "[" + index + 
"]");
+            assignNullToMissingNestedProperty(object, item, 
targetIndexedPropertyName, nestedSourcePropertyName, nestedTargetPropertyName);
+            index++;
+        }
+        return true;
+    }
+
+    private static boolean assignNullToMissingMapProperties(Object object, Map 
map, String targetPathPrefix, String targetRootPropertyName, String 
nestedSourcePropertyName, String nestedTargetPropertyName) {
+        for (Object entryObject : map.entrySet()) {
+            Map.Entry entry = (Map.Entry) entryObject;
+            String targetIndexedPropertyName = 
appendPropertyPath(targetPathPrefix, targetRootPropertyName + "[" + 
entry.getKey() + "]");
+            assignNullToMissingNestedProperty(object, entry.getValue(), 
targetIndexedPropertyName, nestedSourcePropertyName, nestedTargetPropertyName);
+        }
+        return true;
+    }
+
+    private static Object getTargetObject(Object object, String 
targetPathPrefix) {
+        if (targetPathPrefix == null || targetPathPrefix.length() == 0) {
+            return object;
+        }
+
+        Object targetObject = object;
+        for (String propertyName : splitPropertyPath(targetPathPrefix)) {
+            if (targetObject == null) {
+                return null;
+            }
+            targetObject = getPropertyValue(targetObject, propertyName);
+        }
+        return targetObject;
+    }
+
+    private static void assignNullToMissingNestedProperty(Object object, 
Object nestedSource, String targetIndexedPropertyName, String 
nestedSourcePropertyName, String nestedTargetPropertyName) {
+        if (!assignNullToMissingIndexedProperties(object, nestedSource, 
targetIndexedPropertyName, nestedSourcePropertyName, nestedTargetPropertyName) 
&& !containsPropertyPath(nestedSource, nestedSourcePropertyName)) {
+            setPropertyToNull(object, targetIndexedPropertyName + "." + 
nestedTargetPropertyName);
+        }
+    }
+
+    private static String appendPropertyPath(String parentPath, String 
propertyName) {
+        if (parentPath == null || parentPath.length() == 0) {
+            return propertyName;
+        }
+        return parentPath + "." + propertyName;
+    }
+
+    private static boolean bindingSourceContainsProperty(DataBindingSource 
bindingSource, String propertyName, String filter) {
+        String sourcePropertyName = filter == null ? propertyName : filter + 
"." + propertyName;
+        int exactPrefixSegments = filter == null ? 0 : 
splitPropertyPath(filter).length;
+        return containsPropertyPath(bindingSource, sourcePropertyName, 
exactPrefixSegments) || containsPropertyPath(bindingSource, 
checkboxMarkerPropertyName(sourcePropertyName), exactPrefixSegments);
+    }
+
+    private static boolean containsPropertyPath(Object source, String 
propertyName) {
+        return containsPropertyPath(source, propertyName, 0);
+    }
+
+    private static boolean containsPropertyPath(Object source, String 
propertyName, int exactPrefixSegments) {
+        if (containsSourceProperty(source, propertyName)) {
+            return true;
+        }
+        if (containsIndexedPropertyPath(source, propertyName, 
exactPrefixSegments)) {
+            return true;
+        }
+        int separator = propertyPathSeparator(propertyName);
+        if (separator == -1) {
+            return false;
+        }
+        String rootPropertyName = propertyName.substring(0, separator);
+        if (!containsSourceProperty(source, rootPropertyName)) {
+            return containsIndexedNestedPropertyPath(source, rootPropertyName, 
propertyName.substring(separator + 1));
+        }
+        Object nestedSource = getSourcePropertyValue(source, rootPropertyName);
+        String nestedPropertyName = propertyName.substring(separator + 1);
+        if (nestedSource instanceof Collection) {
+            for (Object item : (Collection) nestedSource) {
+                if (containsPropertyPath(item, nestedPropertyName)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        return containsPropertyPath(nestedSource, nestedPropertyName);
+    }
+
+    private static boolean containsIndexedNestedPropertyPath(Object source, 
String rootPropertyName, String nestedPropertyName) {
+        String indexedSourcePropertyPrefix = rootPropertyName + "[";
+        for (String indexedSourcePropertyName : 
getIndexedSourcePropertyNames(source, indexedSourcePropertyPrefix)) {
+            if (containsSourceProperty(source, indexedSourcePropertyName) && 
containsPropertyPath(getSourcePropertyValue(source, indexedSourcePropertyName), 
nestedPropertyName)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean containsIndexedPropertyPath(Object source, String 
propertyName, int exactPrefixSegments) {
+        for (String indexedPropertyName : getSourcePropertyNames(source)) {
+            if (indexedPropertyPathMatches(indexedPropertyName, propertyName, 
exactPrefixSegments)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static void setPropertyToNull(Object object, String propertyName) {
+        String[] propertyNames = splitPropertyPath(propertyName);
+        Object currentObject = object;
+        for (int i = 0; i < propertyNames.length - 1 && currentObject != null; 
i++) {
+            currentObject = getPropertyValue(currentObject, propertyNames[i]);
+        }
+        if (currentObject != null) {
+            setPropertyValueToNull(currentObject, 
propertyNames[propertyNames.length - 1]);
+        }
+    }
+
+    private static Object getPropertyValue(Object object, String propertyName) 
{
+        int bracket = propertyName.indexOf('[');
+        if (bracket == -1) {
+            MetaClass mc = 
GroovySystem.getMetaClassRegistry().getMetaClass(object.getClass());
+            return mc.getProperty(object, propertyName);
+        }
+
+        MetaClass mc = 
GroovySystem.getMetaClassRegistry().getMetaClass(object.getClass());
+        Object indexedProperty = mc.getProperty(object, 
propertyName.substring(0, bracket));
+        return getIndexedValue(indexedProperty, propertyName.substring(bracket 
+ 1, propertyName.indexOf(']', bracket)));
+    }
+
+    private static Object getIndexedValue(Object indexedProperty, String 
index) {
+        if (indexedProperty instanceof List) {
+            return ((List) indexedProperty).get(Integer.parseInt(index));
+        }
+        return ((Map) indexedProperty).get(index);
+    }
+
+    private static void setPropertyValueToNull(Object object, String 
propertyName) {
+        int bracket = propertyName.indexOf('[');
+        if (bracket == -1) {
+            MetaClass mc = 
GroovySystem.getMetaClassRegistry().getMetaClass(object.getClass());
+            mc.setProperty(object, propertyName, null);
+            return;
+        }
+
+        MetaClass mc = 
GroovySystem.getMetaClassRegistry().getMetaClass(object.getClass());
+        Object indexedProperty = mc.getProperty(object, 
propertyName.substring(0, bracket));
+        String index = propertyName.substring(bracket + 1, 
propertyName.indexOf(']', bracket));
+        if (indexedProperty instanceof List) {
+            ((List) indexedProperty).set(Integer.parseInt(index), null);
+        }
+        else {
+            ((Map) indexedProperty).put(index, null);
+        }
     }

Review Comment:
   List index handling in nullMissing paths is not robust: 
getIndexedValue()/setPropertyValueToNull() call Integer.parseInt(index) and 
then List.get/set without guarding. A crafted parameter like members[foo].name 
(or an out-of-range index) can throw 
NumberFormatException/IndexOutOfBoundsException and break secureBindData 
processing. Consider treating non-numeric/out-of-range indices as absent and 
skipping/nulling safely.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to