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
51 changes: 50 additions & 1 deletion api/src/org/labkey/api/action/BaseViewAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.labkey.api.action;

import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.beanutils.ConversionException;
Expand Down Expand Up @@ -50,6 +51,7 @@
import org.springframework.beans.TypeMismatchException;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.lang.Nullable;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingErrorProcessor;
Expand Down Expand Up @@ -178,12 +180,59 @@ public static PropertyValues getPropertyValuesForFormBinding(PropertyValues pvs,
return ret;
}

static final String FORM_DATE_ENCODED_PARAM = "formDataEncoded";

/**
* 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.
*/
static public class ViewActionParameterPropertyValues extends ServletRequestParameterPropertyValues
{

public ViewActionParameterPropertyValues(ServletRequest request) {
this(request, (String)null, (String)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)
{
Object v = formDataPropValue.getValue();
String formDataPropValueStr = v == null ? null : String.valueOf(v);
if (StringUtils.isNotBlank(formDataPropValueStr))
return (Boolean) ConvertUtils.convert(formDataPropValueStr, Boolean.class);
}

return false;
}
}

@Override
public ModelAndView handleRequest(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) throws Exception
{
if (null == getPropertyValues())
setProperties(new ServletRequestParameterPropertyValues(request));
setProperties(new ViewActionParameterPropertyValues(request));
getViewContext().setBindPropertyValues(getPropertyValues());
handleSpecialProperties();

Expand Down
1 change: 0 additions & 1 deletion api/src/org/labkey/api/query/QueryParam.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package org.labkey.api.query;

import org.labkey.api.util.SafeToRenderEnum;
import org.labkey.api.util.URLHelper;

public enum QueryParam implements SafeToRenderEnum
{
Expand Down
29 changes: 29 additions & 0 deletions api/src/org/labkey/api/util/PageFlowUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -2592,6 +2592,19 @@ public static String joinValuesToString(@NotNull List<String> values, char delim
.collect(Collectors.joining(String.valueOf(delimiter)));
}

/**
* Issue 52925: App export to csv/tsv ignores filter with column containing double quote
* Issue 52119: App issues with assay run properties with special characters
* @param encodedKey The encoded form key by client side `encodeFormDataQuote` util
* @return The decoded raw field name
*/
public static String decodeQuoteEncodedFormDataKey(@Nullable String encodedKey)
{
if (encodedKey == null)
return null;
return encodedKey.replaceAll("%22", "\"").replaceAll("%2522", "%22");
}

public static class TestCase extends Assert
{
@Test
Expand Down Expand Up @@ -2909,6 +2922,21 @@ public void encodePath()
assertEquals("a/b/c", PageFlowUtil.encodePath("a/b/c"));
assertEquals("/a/b/c/", PageFlowUtil.encodePath("/a/b/c/"));
}

@Test
public void testDecodeQuoteEncodedFormDataKey()
{
assertEquals("test", decodeQuoteEncodedFormDataKey("test"));
assertEquals("a/b/c", decodeQuoteEncodedFormDataKey("a/b/c"));
assertEquals("a'b.c", decodeQuoteEncodedFormDataKey("a'b.c"));
assertEquals("%", decodeQuoteEncodedFormDataKey("%"));
assertEquals("\"", decodeQuoteEncodedFormDataKey("%22"));
assertEquals("\"\"", decodeQuoteEncodedFormDataKey("%22%22"));
assertEquals("%22", decodeQuoteEncodedFormDataKey("%2522"));
assertEquals("%22%22", decodeQuoteEncodedFormDataKey("%2522%2522"));
assertEquals("%22\"", decodeQuoteEncodedFormDataKey("%2522%22"));
assertEquals("\"22", decodeQuoteEncodedFormDataKey("%2222"));
}
}

/** @return true if the UrlProvider exists. */
Expand Down Expand Up @@ -3129,4 +3157,5 @@ public static HtmlString getDataRegionHtmlForPropertyValues(Map<String, String>
).appendTo(sb);
return HtmlString.unsafe(sb.toString());
}

}
3 changes: 1 addition & 2 deletions assay/src/org/labkey/assay/actions/ImportRunApiAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -634,8 +634,7 @@ private String parsePropertiesKey(String key)
// Issue 52119: account for leading/trailing single quotes and decode double quotes and %
if (key.startsWith("'") && key.endsWith("'"))
key = key.substring(1, key.length()-1);
key = key.replaceAll("%22", "\"");
key = key.replaceAll("%25", "%");
key = PageFlowUtil.decodeQuoteEncodedFormDataKey(key);

return key;
}
Expand Down
7 changes: 6 additions & 1 deletion experiment/src/org/labkey/experiment/ExperimentModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,12 @@ WHERE op.propertyid IN (
results.put("aliquotCount", new SqlSelector(schema, "SELECT COUNT(*) FROM exp.material where aliquotedfromlsid IS NOT NULL").getObject(Long.class));
results.put("sampleNullAmountCount", new SqlSelector(schema, "SELECT COUNT(*) FROM exp.material WHERE storedamount IS NULL").getObject(Long.class));
results.put("sampleNegativeAmountCount", new SqlSelector(schema, "SELECT COUNT(*) FROM exp.material WHERE storedamount < 0").getObject(Long.class));
results.put("sampleUnitsDifferCount", new SqlSelector(schema, "SELECT COUNT(*) from exp.material m JOIN exp.materialSource s ON m.materialsourceid = s.rowid WHERE m.units != s.metricunit\n").getObject(Long.class));
results.put("sampleUnitsDifferCount", new SqlSelector(schema, "SELECT COUNT(*) from exp.material m JOIN exp.materialSource s ON m.materialsourceid = s.rowid WHERE m.units != s.metricunit").getObject(Long.class));

results.put("duplicateSampleMaterialNameCount", new SqlSelector(schema, "SELECT COUNT(*) as duplicateCount FROM " +
"(SELECT name, cpastype FROM exp.material WHERE cpastype <> 'Material' GROUP BY name, cpastype HAVING COUNT(*) > 1) d").getObject(Long.class));
results.put("duplicateSpecimenMaterialNameCount", new SqlSelector(schema, "SELECT COUNT(*) as duplicateCount FROM " +
"(SELECT name, cpastype FROM exp.material WHERE cpastype = 'Material' GROUP BY name, cpastype HAVING COUNT(*) > 1) d").getObject(Long.class));

results.put("dataClassCount", new SqlSelector(schema, "SELECT COUNT(*) FROM exp.dataclass").getObject(Long.class));
results.put("dataClassRowCount", new SqlSelector(schema, "SELECT COUNT(*) FROM exp.data WHERE classid IN (SELECT rowid FROM exp.dataclass)").getObject(Long.class));
Expand Down