Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d56b062
PageFlowUtil.encodeFormName() and decodeFormName()
labkey-matthewb Mar 19, 2026
7cb411f
Merge remote-tracking branch 'origin/develop' into fb_encodeFormName
labkey-matthewb Mar 21, 2026
9946543
EscapeUtil.getFormFieldName()
labkey-matthewb Mar 21, 2026
21bd355
Merge remote-tracking branch 'origin/develop' into fb_encodeFormName
labkey-matthewb Mar 23, 2026
1a45338
BaseViewAction.getFiles()
labkey-matthewb Mar 23, 2026
e4a743a
Merge remote-tracking branch 'origin/develop' into fb_encodeFormName
labkey-matthewb Mar 23, 2026
734ece4
undo incorrect search/replace
labkey-matthewb Mar 23, 2026
51db816
prefer getProperty() over getRequest().getParameter()
labkey-matthewb Mar 23, 2026
d969731
PageFlowUtil.getFileMap()
labkey-matthewb Mar 24, 2026
7d1a56b
Merge remote-tracking branch 'origin/develop' into fb_encodeFormName
labkey-matthewb Mar 24, 2026
b56f038
Bump @labkey/components
labkey-nicka Mar 25, 2026
bc925a0
WARNING for MultipartRequest methods directly
labkey-matthewb Mar 25, 2026
f8cae4f
Merge remote-tracking branch 'origin/fb_encodeFormName' into fb_encod…
labkey-matthewb Mar 25, 2026
0653561
Merge remote-tracking branch 'origin/develop' into fb_encodeFormName
labkey-matthewb Mar 25, 2026
2a3e7b4
WARNING for MultipartRequest methods directly
labkey-matthewb Mar 25, 2026
6ed42aa
revert unrelated change
labkey-matthewb Mar 25, 2026
d36f745
Missed this overriden version of getColumnByFormFieldName()
labkey-matthewb Mar 25, 2026
c7b22c3
Merge branch 'develop' into fb_encodeFormName
labkey-nicka Mar 25, 2026
4205ffa
Bump @labkey/components
labkey-nicka Mar 25, 2026
3d41479
Merge remote-tracking branch 'origin/develop' into fb_encodeFormName
labkey-matthewb Mar 26, 2026
2d78208
CR
labkey-matthewb Mar 26, 2026
9127b05
Bump @labkey packages
labkey-nicka Mar 26, 2026
2a19108
nits
labkey-nicka Mar 26, 2026
dd4897e
Fix bad cast of getPkVal() to String
labkey-matthewb Mar 26, 2026
73a282f
extract shared code
labkey-matthewb Mar 26, 2026
1e81641
Merge remote-tracking branch 'origin/develop' into fb_encodeFormName
labkey-matthewb Mar 27, 2026
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
15 changes: 5 additions & 10 deletions api/src/org/labkey/api/action/AbstractFileUploadAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package org.labkey.api.action;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.NotNull;
import org.labkey.api.util.ExceptionUtil;
Expand All @@ -42,7 +41,6 @@
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
Expand Down Expand Up @@ -192,19 +190,16 @@
return;
}

HttpServletRequest basicRequest = getViewContext().getRequest();

// Parameter name (String) -> File on disk/original file name Pair
Map<String, Pair<FileLike, String>> savedFiles = new HashMap<>();

if (basicRequest instanceof MultipartHttpServletRequest request)
if (getViewContext().getRequest() instanceof MultipartHttpServletRequest)
{

Iterator<String> nameIterator = request.getFileNames();
while (nameIterator.hasNext())
Map<String, MultipartFile> fileMap = getFileMap();
for (var e : fileMap.entrySet())
{
String formElementName = nameIterator.next();
MultipartFile file = request.getFile(formElementName);
var formElementName = e.getKey();
var file = e.getValue();
String filename = file.getOriginalFilename();

try (InputStream input = file.getInputStream())
Expand Down Expand Up @@ -243,7 +238,7 @@
}
catch (UploadException e)
{
error(writer, "Must include the same number of fileName and fileContent parameter values", HttpServletResponse.SC_BAD_REQUEST);

Check warning

Code scanning / CodeQL

Cross-site scripting Medium

Cross-site scripting vulnerability due to a
user-provided value
.
}
}
}
Expand Down
46 changes: 10 additions & 36 deletions api/src/org/labkey/api/action/BaseViewAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
Expand Down Expand Up @@ -185,51 +186,26 @@ public static PropertyValues getPropertyValuesForFormBinding(PropertyValues pvs,
return ret;
}

static final String FORM_DATE_ENCODED_PARAM = "formDataEncoded";
Copy link
Copy Markdown
Contributor

@labkey-nicka labkey-nicka Mar 26, 2026

Choose a reason for hiding this comment

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

Seems like we need more client-side updates as formDataEncoded is no longer necessary. This encoding is being performed for all forms.

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.

I was hoping encodeFormDataQuote() would cover the client-side code change.


/**
* When a double quote is encountered in a multipart/form-data context, it is encoded as %22 using URL-encoding by browsers.
* This process replaces the double quote with its hexadecimal equivalent in a URL-safe format, preventing it from being misinterpreted as the end of a value or a boundary.
* The consequence of such encoding is we can't distinguish '"' from the actual '%22' in parameter name.
* As a workaround, a client-side util `encodeFormDataQuote` is used to convert %22 to %2522 and " to %22 explicitly, while passing in an additional param formDataEncoded=true.
* This class converts those encoded param names back to its decoded form during PropertyValues binding.
* See Issue 52827, 52925 and 52119 for more information.
*/
/// Some characters can be mishandled by the browser in multipart/formdata requests (e.g. doublequote and backslask).
/// We support an encoding from fields to avoid these characters, see {@link PageFlowUtil#encodeFormName} and {@link PageFlowUtil#decodeFormName}.
static public class ViewActionParameterPropertyValues extends ServletRequestParameterPropertyValues
{

public ViewActionParameterPropertyValues(ServletRequest request) {
this(request, null, null);
}

public ViewActionParameterPropertyValues(ServletRequest request, @Nullable String prefix, @Nullable String prefixSeparator)
{
super(request, prefix, prefixSeparator);
if (isFormDataEncoded())
{
for (int i = 0; i < getPropertyValues().length; i++)
{
PropertyValue formDataPropValue = getPropertyValues()[i];
String propValueName = formDataPropValue.getName();
String decoded = PageFlowUtil.decodeQuoteEncodedFormDataKey(propValueName);
if (!propValueName.equals(decoded))
setPropertyValueAt(new PropertyValue(decoded, formDataPropValue.getValue()), i);
}
}
}

private boolean isFormDataEncoded()
{
PropertyValue formDataPropValue = getPropertyValue(FORM_DATE_ENCODED_PARAM);
if (formDataPropValue != null)
for (int i = 0; i < getPropertyValues().length; i++)
{
Object v = formDataPropValue.getValue();
String formDataPropValueStr = v == null ? null : String.valueOf(v);
if (StringUtils.isNotBlank(formDataPropValueStr))
return (Boolean) ConvertUtils.convert(formDataPropValueStr, Boolean.class);
PropertyValue formDataPropValue = getPropertyValues()[i];
String propValueName = formDataPropValue.getName();
String decoded = PageFlowUtil.decodeFormName(propValueName);
if (!propValueName.equals(decoded))
setPropertyValueAt(new PropertyValue(decoded, formDataPropValue.getValue()), i);
}

return false;
}
}

Expand Down Expand Up @@ -725,9 +701,7 @@ public <T> T convertIfNecessary(Object value, Class<T> requiredType, MethodParam
*/
protected Map<String, MultipartFile> getFileMap()
{
if (getViewContext().getRequest() instanceof MultipartHttpServletRequest)
return ((MultipartHttpServletRequest)getViewContext().getRequest()).getFileMap();
return Collections.emptyMap();
return PageFlowUtil.getFileMap(getViewContext().getRequest());
}

protected List<AttachmentFile> getAttachmentFileList()
Expand Down
3 changes: 2 additions & 1 deletion api/src/org/labkey/api/assay/AssayFileWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.labkey.api.query.AbstractQueryUpdateService;
import org.labkey.api.util.FileUtil;
import org.labkey.api.util.NetworkDrive;
import org.labkey.api.util.PageFlowUtil;
import org.labkey.api.view.ViewContext;
import org.labkey.vfs.FileLike;
import org.springframework.web.multipart.MultipartFile;
Expand Down Expand Up @@ -233,7 +234,7 @@ public Map<String, FileLike> savePostedFiles(ContextType context, @NotNull Set<S
Set<String> originalFileNames = new HashSet<>();
if (context.getRequest() instanceof MultipartHttpServletRequest multipartRequest)
{
Iterator<Map.Entry<String, List<MultipartFile>>> iter = multipartRequest.getMultiFileMap().entrySet().iterator();
Iterator<Map.Entry<String, List<MultipartFile>>> iter = PageFlowUtil.getMultiFileMap(context.getRequest()).entrySet().iterator();
Deque<FileLike> overflowFiles = new ArrayDeque<>(); // using a deque for easy removal of single elements
Set<String> unusedParameterNames = new HashSet<>(parameterNames);
while (iter.hasNext())
Expand Down
5 changes: 4 additions & 1 deletion api/src/org/labkey/api/assay/actions/AssayRunUploadForm.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.labkey.api.action.BaseViewAction;
import org.labkey.api.assay.AbstractAssayProvider;
import org.labkey.api.assay.AssayDataCollector;
import org.labkey.api.assay.AssayFileWriter;
Expand Down Expand Up @@ -61,6 +62,7 @@
import org.labkey.api.study.assay.ParticipantVisitResolverType;
import org.labkey.api.util.FileUtil;
import org.labkey.api.util.GUID;
import org.labkey.api.util.PageFlowUtil;
import org.labkey.api.view.ActionURL;
import org.labkey.api.view.NotFoundException;
import org.labkey.api.view.UnauthorizedException;
Expand Down Expand Up @@ -360,10 +362,11 @@ public Map<DomainProperty, FileLike> getAdditionalPostedFiles(List<? extends Dom
File assayDirectory = getAssayDirectory(getContainer(), null);

// Hidden values in form containing previously uploaded files if the previous upload resulted in error
var fileMap = PageFlowUtil.getFileMap(request);
for (String fileParam : filePdNames)
{
DomainProperty domainProperty = fileParameters.get(fileParam);
MultipartFile multiFile = request.getFileMap().get(fileParam);
MultipartFile multiFile = fileMap.get(fileParam);

// If the file is removed from form after error, override hidden file name with an empty file
if (null != multiFile && multiFile.getOriginalFilename().isEmpty())
Expand Down
25 changes: 9 additions & 16 deletions api/src/org/labkey/api/data/TableViewForm.java
Original file line number Diff line number Diff line change
Expand Up @@ -553,24 +553,21 @@ public CaseInsensitiveHashMap<Object> getTypedColumns(boolean includeUntyped)

for (ColumnInfo column : getTable().getColumns())
{
var fieldName = getFormFieldName(column);

if (hasTypedValue(column))
{
values.put(column.getName(), getTypedValue(column));
}
else if (includeUntyped && _stringValues.containsKey(getFormFieldName(column)))
else if (includeUntyped && _stringValues.containsKey(fieldName))
{
values.put(column.getName(), _stringValues.get(getFormFieldName(column)));
values.put(column.getName(), _stringValues.get(fieldName));
}
else if (getRequest() instanceof MultipartHttpServletRequest request)
{
String fieldName = getMultiPartFormFieldName(column);
Object typedValue = _getTypedValues().get(fieldName);

if (typedValue != null)
values.put(column.getName(), typedValue);
else if (File.class.equals(column.getJavaClass()))
if (File.class.equals(column.getJavaClass()))
{
MultipartFile file = request.getFile(fieldName);
MultipartFile file = PageFlowUtil.getFileMap(request).get(fieldName);
if (file != null)
{
// Check if the file was removed
Expand All @@ -587,10 +584,11 @@ else if (File.class.equals(column.getJavaClass()))
ColumnInfo mvColumn = getTable().getColumn(column.getMvColumnName());
if (null != mvColumn)
{
var mvFieldName = getFormFieldName(mvColumn);
if (hasTypedValue(mvColumn))
values.put(mvColumn.getName(), getTypedValue(mvColumn));
else if (includeUntyped && _stringValues.containsKey(getFormFieldName(mvColumn)))
values.put(mvColumn.getName(), _stringValues.get(getFormFieldName(mvColumn)));
else if (includeUntyped && _stringValues.containsKey(mvFieldName))
values.put(mvColumn.getName(), _stringValues.get(mvFieldName));
}
}
}
Expand Down Expand Up @@ -739,11 +737,6 @@ public String getFormFieldName(@NotNull ColumnInfo column)
return column.getName();
}

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

@Nullable
public ColumnInfo getColumnByFormFieldName(@NotNull String name)
{
Expand Down
50 changes: 2 additions & 48 deletions api/src/org/labkey/api/dataiterator/DataIteratorUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,57 +141,14 @@ 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
public enum MatchType
private 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;
}

/**
* 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
* @return If rowMap has been updated
*/
public boolean updateRowMap(@NotNull ColumnInfo col, Map<String, Object> rowMap)
{
return false;
}
low
}


Expand All @@ -209,9 +166,6 @@ 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
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import org.labkey.api.data.TableInfo;
import org.labkey.api.data.validator.ColumnValidator;
import org.labkey.api.data.validator.ColumnValidators;
import org.labkey.api.data.validator.RequiredValidator;
import org.labkey.api.exp.PropertyDescriptor;
import org.labkey.api.exp.PropertyType;
import org.labkey.api.exp.api.ExperimentService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ public boolean handlePost(FormType domainIdForm, BindException errors) throws Ex
*/
protected String encodePropertyValues(FormType domainIdForm, String propName) throws IOException
{
return domainIdForm.getRequest().getParameter(propName);
return (String)getProperty(propName);
}

@Override
Expand Down
2 changes: 0 additions & 2 deletions api/src/org/labkey/api/exp/property/DomainUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -1587,8 +1587,6 @@ public static ValidationException validateProperties(@Nullable Domain domain, @N
else
{
altNameMap.put(name, name);
altNameMap.put(DataIteratorUtil.MatchType.multiPartFormData.getMatchedName(name), name);
altNameMap.put(name.replaceAll("%22", "\""), name);
}
}

Expand Down
5 changes: 5 additions & 0 deletions api/src/org/labkey/api/jsp/JspBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ public static HtmlString h(URLHelper url)
return h(url == null ? null : url.toString());
}

public static HtmlString hname(String name)
{
return HtmlString.of(PageFlowUtil.encodeFormName(name));
}

// Note: If you have a stream, use LabKeyCollectors.toJsonArray()
public static JSONArray toJsonArray(Collection<?> c)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ else if (_target != null)
}
else if (getViewContext().getRequest() instanceof MultipartHttpServletRequest)
{
Map<String, MultipartFile> files = ((MultipartHttpServletRequest)getViewContext().getRequest()).getFileMap();
Map<String, MultipartFile> files = getFileMap();
MultipartFile multipartfile = null==files ? null : files.get("file");
if (null != multipartfile && multipartfile.getSize() > 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package org.labkey.api.query;

import org.apache.commons.beanutils.ConversionException;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down
Loading
Loading