Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions api/src/org/labkey/api/data/TableViewForm.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import org.labkey.api.action.NullSafeBindException;
import org.labkey.api.action.SpringActionController;
import org.labkey.api.collections.CaseInsensitiveHashMap;
import org.labkey.api.dataiterator.DataIteratorUtil;
import org.labkey.api.query.QueryUpdateForm;
import org.labkey.api.query.SchemaKey;
import org.labkey.api.security.permissions.DeletePermission;
import org.labkey.api.security.permissions.InsertPermission;
Expand Down Expand Up @@ -572,6 +574,26 @@ public Map<String,Object> getTypedColumns(boolean includeUntyped)
values.put(column.getName(), getTypedValue(column));
else if (includeUntyped && contains(column))
values.put(column.getName(), get(column));
else if (column.getName().contains("\"") && getViewContext().getRequest() instanceof MultipartHttpServletRequest)
{
String quoteEncodedFieldName = getMultiPartFormFieldName(column);
boolean isFileColFileRemoved = false;
if (values.get(column.getName()) == null && File.class.equals(column.getJavaClass()))
{
MultipartHttpServletRequest request = (MultipartHttpServletRequest) getRequest();
MultipartFile f = request.getFile(quoteEncodedFieldName);
isFileColFileRemoved = f != null && (f.getOriginalFilename() == null || f.getOriginalFilename().isEmpty());
}

if (isFileColFileRemoved)
values.put(column.getName(), null);
else
{
Object value = getTypedValues().get(quoteEncodedFieldName);
if (value != null)
values.put(column.getName(), value);
}
Comment on lines +583 to +595
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like a lot of duplicate code from what is already there below for handling the non-slash column name case (lines 599-607). it seems like the main part is the field name to use in request.getFile(). Could these blocks of code be collapsed and have the conditional part be using either getMultiPartFormFieldName or getFormFieldName? Then after we have the MultipartFile, the rest of the code seems pretty similar.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was how it was written in a previous commit, but that broke LKS attachment update. The different part is what value to use for file field: string or File. The only shared code is the following 2 lines so I removed the shared util in the last commit.
MultipartHttpServletRequest request = (MultipartHttpServletRequest) getRequest(); MultipartFile f = request.getFile(quoteEncodedFieldName);

}

// Check if there was a file uploaded for the column's value
if (values.get(column.getName()) == null && File.class.equals(column.getJavaClass()) && getRequest() instanceof MultipartHttpServletRequest)
Expand Down Expand Up @@ -765,6 +787,12 @@ public String getFormFieldName(@NotNull ColumnInfo column)
return column.getPropertyName();
}

public String getMultiPartFormFieldName(@NotNull ColumnInfo column)
{
return getFormFieldName(column);
}


@Nullable
public ColumnInfo getColumnByFormFieldName(@NotNull String name)
{
Expand Down
60 changes: 58 additions & 2 deletions api/src/org/labkey/api/dataiterator/DataIteratorUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.Assert;
import org.junit.Test;
Expand Down Expand Up @@ -56,7 +57,6 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.function.Function;
Expand Down Expand Up @@ -141,7 +141,60 @@ public static Map<String,ColumnInfo> createTableMap(TableInfo target, boolean us

// rank of a match of import column NAME matching various properties of target column
// MatchType.low is used for matches based on something other than name
enum MatchType {propertyuri, name, alias, jdbcname, tsvColumn, low}
public enum MatchType
{
propertyuri,
name,
alias,
jdbcname,
tsvColumn,
multiPartFormData()
{
@Override
public String getMatchedName(@Nullable String name)
{
if (name == null)
return null;
// " is encoded as %22 when content-type is "multipart/form-data" (but is not otherwise encoded so decode() does not work)
return name.replaceAll("\"", "%22");
}

@Override
public boolean updateRowMap(@NotNull ColumnInfo col, Map<String, Object> rowMap)
{
if (col.getName().contains("\"") && File.class.equals(col.getJavaClass()))
{
// Issue 52827: File/attachment fields with special characters
String quoteEncodedName = DataIteratorUtil.MatchType.multiPartFormData.getMatchedName(col.getName());
if (rowMap.containsKey(quoteEncodedName))
{
rowMap.put(col.getName(), rowMap.get(quoteEncodedName));
rowMap.remove(quoteEncodedName);
return true;
}
}
return false;
}
},
low;

public String getMatchedName(@Nullable String name)
{
return name;
}
Comment thread
cnathe marked this conversation as resolved.

/**
* Update rowMap content based on passed in col.
* For example, the original rowMap may contain encoded field name. This util substitute the key in rowMap to reflect the actual col name
* @param col
* @param rowMap
* @return If rowMap has been updated
*/
public boolean updateRowMap(@NotNull ColumnInfo col, Map<String, Object> rowMap)
{
return false;
}
}


/**
Expand All @@ -158,6 +211,9 @@ protected static Map<String,Pair<ColumnInfo,MatchType>> _createTableMap(TableInf

Map<String, Pair<ColumnInfo,MatchType>> targetAliasesMap = new CaseInsensitiveHashMap<>(cols.size()*4);

for (ColumnInfo col : cols)
targetAliasesMap.put(MatchType.multiPartFormData.getMatchedName(col.getName()), new Pair<>(col, MatchType.multiPartFormData));

// should this be under the useImportAliases flag???
for (ColumnInfo col : cols)
{
Expand Down
24 changes: 20 additions & 4 deletions api/src/org/labkey/api/exp/property/DomainUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.labkey.api.data.SimpleFilter;
import org.labkey.api.data.TableInfo;
import org.labkey.api.data.TableSelector;
import org.labkey.api.dataiterator.DataIteratorUtil;
import org.labkey.api.defaults.DefaultValueService;
import org.labkey.api.exp.ChangePropertyDescriptorException;
import org.labkey.api.exp.DomainDescriptor;
Expand Down Expand Up @@ -1337,6 +1338,7 @@ public static ValidationException validateProperties(@Nullable Domain domain, @N
: new CaseInsensitiveHashSet(updates.getReservedFieldNames());
Set<String> reservedPrefixes = (null != domain && null != domainKind) ? domainKind.getReservedPropertyNamePrefixes() : updates.getReservedFieldNamePrefixes();
Map<String, Integer> namePropertyIdMap = new CaseInsensitiveHashMap<>();
Map<String, String> altNameMap = new CaseInsensitiveHashMap<>();
ValidationException exception = new ValidationException();
Map<Integer, String> propertyIdNameMap = getOriginalFieldPropertyIdNameMap(orig);//key: orig property id, value : orig field name

Expand All @@ -1346,7 +1348,7 @@ public static ValidationException validateProperties(@Nullable Domain domain, @N

if (null == name || name.trim().isEmpty())
{
exception.addError(new SimpleValidationError(getDomainErrorMessage(updates,"Please provide a name for each field.")));
exception.addError(new SimpleValidationError(getDomainErrorMessage(updates, "Please provide a name for each field.")));
continue;
}

Expand Down Expand Up @@ -1374,7 +1376,7 @@ public static ValidationException validateProperties(@Nullable Domain domain, @N
String origFieldName = (null != propertyIdNameMap ? propertyIdNameMap.get(field.getPropertyId()) : null);
if (field.getPropertyId() <= 0 || !name.equalsIgnoreCase(origFieldName))
{
exception.addFieldError(name, getDomainErrorMessage(updates,("'" + name + "' is a reserved field name in '" + updates.getName() + "'.")));
exception.addFieldError(name, getDomainErrorMessage(updates, ("'" + name + "' is a reserved field name in '" + updates.getName() + "'.")));
}
continue;
}
Expand All @@ -1391,18 +1393,32 @@ public static ValidationException validateProperties(@Nullable Domain domain, @N

if (namePropertyIdMap.containsKey(name))
{
String errorMsg = getDomainErrorMessage(updates,"The field name '" + name + "' is already taken. Please provide a unique name for each field.");
String errorMsg = getDomainErrorMessage(updates, "The field name '" + name + "' is already taken. Please provide a unique name for each field.");
PropertyValidationError propertyValidationError = new PropertyValidationError(errorMsg, name, field.getPropertyId());
exception.addError(propertyValidationError);
}
else
{
namePropertyIdMap.put(name, field.getPropertyId());

// Issue 52827: File/attachment fields with special characters
if (altNameMap.containsKey(name))
{
String errorMsg = getDomainErrorMessage(updates, "The field name '" + name + "' cannot be used with another field '" + altNameMap.get(name) + "'. Please provide a different name for the field.");
PropertyValidationError propertyValidationError = new PropertyValidationError(errorMsg, name, field.getPropertyId());
exception.addError(propertyValidationError);
}
else
{
altNameMap.put(name, name);
altNameMap.put(DataIteratorUtil.MatchType.multiPartFormData.getMatchedName(name), name);
altNameMap.put(name.replaceAll("%22", "\""), name);
}
}

if (field.getValueExpression() != null && field.getValueExpression().trim().length() > 4000)
{
exception.addFieldError(name, getDomainErrorMessage(updates,"The value expression for '" + name
exception.addFieldError(name, getDomainErrorMessage(updates, "The value expression for '" + name
+ "' is too long (" + field.getValueExpression().trim().length() + " characters). Please limit to 4000 characters."));
}
}
Expand Down
7 changes: 7 additions & 0 deletions api/src/org/labkey/api/query/QueryUpdateForm.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.labkey.api.data.ColumnInfo;
import org.labkey.api.data.TableInfo;
import org.labkey.api.data.TableViewForm;
import org.labkey.api.dataiterator.DataIteratorUtil;
import org.labkey.api.view.ViewContext;
import org.springframework.validation.BindException;

Expand Down Expand Up @@ -83,4 +84,10 @@ public String getFormFieldName(@NotNull ColumnInfo column)
{
return (_ignorePrefix ? "" : PREFIX) + column.getName();
}

@Override
public String getMultiPartFormFieldName(@NotNull ColumnInfo column)
{
return (_ignorePrefix ? "" : PREFIX) + DataIteratorUtil.MatchType.multiPartFormData.getMatchedName(column.getName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ private List<Map<String, Object>> resolveRows(
ColumnInfo col = columnInfoMap.get(entry.getKey());
if (col != null && !row.containsKey(entry.getKey()))
{
if (DataIteratorUtil.MatchType.multiPartFormData.updateRowMap(col, row))
continue;

// use column names for existing row values
row.put(col.getName(), entry.getValue());
}
Expand Down
38 changes: 25 additions & 13 deletions query/src/org/labkey/query/controllers/QueryController.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
import org.labkey.api.data.dialect.SqlDialect;
import org.labkey.api.dataiterator.DataIteratorBuilder;
import org.labkey.api.dataiterator.DataIteratorContext;
import org.labkey.api.dataiterator.DataIteratorUtil;
import org.labkey.api.dataiterator.DetailedAuditLogDataIterator;
import org.labkey.api.dataiterator.ListofMapsDataIterator;
import org.labkey.api.exceptions.OptimisticConflictException;
Expand Down Expand Up @@ -4491,7 +4492,7 @@ protected JSONObject executeJson(JSONObject json, CommandType commandType, boole
// Use shallow copy since jsonObj.toMap() will translate contained JSONObjects into Maps, which we don't want
JsonUtil.fillMapShallow(jsonObj, rowMap);
if (allowRowAttachments())
addRowAttachments(rowMap, idx, commandIndex);
addRowAttachments(table, rowMap, idx, commandIndex);

rowsToProcess.add(rowMap);
rowsAffected++;
Expand Down Expand Up @@ -4643,34 +4644,42 @@ protected boolean allowRowAttachments()
return false;
}

private void addRowAttachments(Map<String, Object> rowMap, int rowIndex, @Nullable Integer commandIndex)
private void addRowAttachments(TableInfo tableInfo, Map<String, Object> rowMap, int rowIndex, @Nullable Integer commandIndex)
{
if (getFileMap() != null)
{
for (Map.Entry<String, MultipartFile> fileEntry : getFileMap().entrySet())
{
// Allow for the fileMap key to include the row index, and optionally command index, for defining
// which row to attach this file to
String fieldKey = fileEntry.getKey();
int delimIndex = fieldKey.lastIndexOf(ROW_ATTACHMENT_INDEX_DELIM);
if (delimIndex > -1)
String fullKey = fileEntry.getKey();
String fieldKey = fullKey;
// Issue 52827: Cannot attach a file if the field name contains ::
// use lastIndexOf instead of split to get the proper parts
int lastDelimIndex = fullKey.lastIndexOf(ROW_ATTACHMENT_INDEX_DELIM);
if (lastDelimIndex > -1)
{
String[] parts = fileEntry.getKey().split(ROW_ATTACHMENT_INDEX_DELIM);
String fieldKeyExcludeIndex = fullKey.substring(0, lastDelimIndex);
String fieldRowIndex = fullKey.substring(lastDelimIndex + ROW_ATTACHMENT_INDEX_DELIM.length());
if (!fieldRowIndex.equals(rowIndex+"")) continue;

if (commandIndex == null)
{
// Single command, so we're parsing file names in the format of: FileField::0
fieldKey = parts[0];
String fieldRowIndex = parts[1];
if (!fieldRowIndex.equals(rowIndex+"")) continue;
fieldKey = fieldKeyExcludeIndex;
}
else
{
// Multi-command, so we're parsing file names in the format of: FileField::0::1
fieldKey = parts[0];
String fieldCommandIndex = parts[1];
String fieldRowIndex = parts[2];
if (!fieldCommandIndex.equals(commandIndex+"") || !fieldRowIndex.equals(rowIndex+""))
int subDelimIndex = fieldKeyExcludeIndex.lastIndexOf(ROW_ATTACHMENT_INDEX_DELIM);
if (subDelimIndex > -1)
{
fieldKey = fieldKeyExcludeIndex.substring(0, subDelimIndex);
String fieldCommandIndex = fieldKeyExcludeIndex.substring(subDelimIndex + ROW_ATTACHMENT_INDEX_DELIM.length());
if (!fieldCommandIndex.equals(commandIndex+""))
continue;
}
else
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the correct fix is to remove the encoded characters here (early) rather than trying to match them later. Ideally the encoded chars shouldn't "leak" out of code that is pulling values out of the request object.

Then we wouldn't have to handle these characters later.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The challenge here is that a field might actually contain "%22". Both field" and field%22 will come in as field%22 and we don't know when to decode vs not here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤯

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really an HTTP bug (it's not OK to escape " to %22 without escaping %). So this would break any column with " in its name in a type with a file column then. Not just the file column.

Since these columns could never map to a java property. I'd propose changing BaseColumnInfo.getPropertyName() to replace " with %QUOTE% or something. That should solve the problem by never using the " in the input name. That would fix the dataregion case, anyway. I'm not sure what the editable grid does.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Editable grid in apps currently doesn't support file fields. The paths supported in Apps are editing detail editing and bulk ediitng. Detail editing uses updateRows api with formData (file, plus json data for other row data) while bulk update uses saveRows. For Apps, only the file field is impacted since other fields are in json. This approach of replace " with %QUOTE% could work for Apps too, but it's also more error prone. Also, it's hard to pick a substitute that absolutely won't collide with real field names, a field might just actually contain %QUOTE%.

continue;
}
}
Expand All @@ -4679,6 +4688,9 @@ private void addRowAttachments(Map<String, Object> rowMap, int rowIndex, @Nullab
rowMap.put(fieldKey, file.isEmpty() ? null : file);
}
}

for (ColumnInfo col : tableInfo.getColumns())
DataIteratorUtil.MatchType.multiPartFormData.updateRowMap(col, rowMap);
}

protected boolean isSuccessOnValidationError()
Expand Down