Skip to content

Commit 2148c77

Browse files
Merge remote-tracking branch 'origin/develop' into 26.3_fb_blank_sql
2 parents 8462bf9 + f6e0efe commit 2148c77

173 files changed

Lines changed: 6523 additions & 4110 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api/src/org/labkey/api/action/AbstractFileUploadAction.java

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
package org.labkey.api.action;
1717

18-
import jakarta.servlet.http.HttpServletRequest;
1918
import jakarta.servlet.http.HttpServletResponse;
2019
import org.jetbrains.annotations.NotNull;
2120
import org.labkey.api.util.ExceptionUtil;
@@ -42,7 +41,6 @@
4241
import java.io.Writer;
4342
import java.nio.charset.StandardCharsets;
4443
import java.util.HashMap;
45-
import java.util.Iterator;
4644
import java.util.Map;
4745

4846
/**
@@ -192,19 +190,16 @@ private void export(FORM form, HttpServletResponse response) throws Exception
192190
return;
193191
}
194192

195-
HttpServletRequest basicRequest = getViewContext().getRequest();
196-
197193
// Parameter name (String) -> File on disk/original file name Pair
198194
Map<String, Pair<FileLike, String>> savedFiles = new HashMap<>();
199195

200-
if (basicRequest instanceof MultipartHttpServletRequest request)
196+
if (getViewContext().getRequest() instanceof MultipartHttpServletRequest)
201197
{
202-
203-
Iterator<String> nameIterator = request.getFileNames();
204-
while (nameIterator.hasNext())
198+
Map<String, MultipartFile> fileMap = getFileMap();
199+
for (var e : fileMap.entrySet())
205200
{
206-
String formElementName = nameIterator.next();
207-
MultipartFile file = request.getFile(formElementName);
201+
var formElementName = e.getKey();
202+
var file = e.getValue();
208203
String filename = file.getOriginalFilename();
209204

210205
try (InputStream input = file.getInputStream())

api/src/org/labkey/api/action/BaseViewAction.java

Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
import java.util.Collection;
7777
import java.util.Collections;
7878
import java.util.HashMap;
79+
import java.util.LinkedHashMap;
7980
import java.util.List;
8081
import java.util.Map;
8182
import java.util.function.Predicate;
@@ -185,51 +186,26 @@ public static PropertyValues getPropertyValuesForFormBinding(PropertyValues pvs,
185186
return ret;
186187
}
187188

188-
static final String FORM_DATE_ENCODED_PARAM = "formDataEncoded";
189189

190-
/**
191-
* When a double quote is encountered in a multipart/form-data context, it is encoded as %22 using URL-encoding by browsers.
192-
* 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.
193-
* The consequence of such encoding is we can't distinguish '"' from the actual '%22' in parameter name.
194-
* 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.
195-
* This class converts those encoded param names back to its decoded form during PropertyValues binding.
196-
* See Issue 52827, 52925 and 52119 for more information.
197-
*/
190+
/// Some characters can be mishandled by the browser in multipart/formdata requests (e.g. doublequote and backslask).
191+
/// We support an encoding from fields to avoid these characters, see {@link PageFlowUtil#encodeFormName} and {@link PageFlowUtil#decodeFormName}.
198192
static public class ViewActionParameterPropertyValues extends ServletRequestParameterPropertyValues
199193
{
200-
201194
public ViewActionParameterPropertyValues(ServletRequest request) {
202195
this(request, null, null);
203196
}
204197

205198
public ViewActionParameterPropertyValues(ServletRequest request, @Nullable String prefix, @Nullable String prefixSeparator)
206199
{
207200
super(request, prefix, prefixSeparator);
208-
if (isFormDataEncoded())
209-
{
210-
for (int i = 0; i < getPropertyValues().length; i++)
211-
{
212-
PropertyValue formDataPropValue = getPropertyValues()[i];
213-
String propValueName = formDataPropValue.getName();
214-
String decoded = PageFlowUtil.decodeQuoteEncodedFormDataKey(propValueName);
215-
if (!propValueName.equals(decoded))
216-
setPropertyValueAt(new PropertyValue(decoded, formDataPropValue.getValue()), i);
217-
}
218-
}
219-
}
220-
221-
private boolean isFormDataEncoded()
222-
{
223-
PropertyValue formDataPropValue = getPropertyValue(FORM_DATE_ENCODED_PARAM);
224-
if (formDataPropValue != null)
201+
for (int i = 0; i < getPropertyValues().length; i++)
225202
{
226-
Object v = formDataPropValue.getValue();
227-
String formDataPropValueStr = v == null ? null : String.valueOf(v);
228-
if (StringUtils.isNotBlank(formDataPropValueStr))
229-
return (Boolean) ConvertUtils.convert(formDataPropValueStr, Boolean.class);
203+
PropertyValue formDataPropValue = getPropertyValues()[i];
204+
String propValueName = formDataPropValue.getName();
205+
String decoded = PageFlowUtil.decodeFormName(propValueName);
206+
if (!propValueName.equals(decoded))
207+
setPropertyValueAt(new PropertyValue(decoded, formDataPropValue.getValue()), i);
230208
}
231-
232-
return false;
233209
}
234210
}
235211

@@ -725,9 +701,7 @@ public <T> T convertIfNecessary(Object value, Class<T> requiredType, MethodParam
725701
*/
726702
protected Map<String, MultipartFile> getFileMap()
727703
{
728-
if (getViewContext().getRequest() instanceof MultipartHttpServletRequest)
729-
return ((MultipartHttpServletRequest)getViewContext().getRequest()).getFileMap();
730-
return Collections.emptyMap();
704+
return PageFlowUtil.getFileMap(getViewContext().getRequest());
731705
}
732706

733707
protected List<AttachmentFile> getAttachmentFileList()

api/src/org/labkey/api/admin/AdminUrls.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public interface AdminUrls extends UrlProvider
6868
ActionURL getCspReportToURL();
6969

7070
ActionURL getAllowedExternalRedirectHostsURL();
71+
ActionURL getDeleteEncryptedContentURL();
7172

7273
/**
7374
* Simply adds an "Admin Console" link to nav trail if invoked in the root container. Otherwise, root is unchanged.

api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,7 @@ else if (entry.getKey().equalsIgnoreCase(ProvenanceService.PROVENANCE_INPUT_PROP
867867
if (PropertyType.MULTI_CHOICE == pd.getPropertyType())
868868
{
869869
o = MultiChoice.Converter.getInstance().convert(MultiChoice.Array.class, o);
870+
map.put(pd.getName(), o);
870871
}
871872
else if (o instanceof String)
872873
{

api/src/org/labkey/api/assay/AssayFileWriter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.labkey.api.query.AbstractQueryUpdateService;
3232
import org.labkey.api.util.FileUtil;
3333
import org.labkey.api.util.NetworkDrive;
34+
import org.labkey.api.util.PageFlowUtil;
3435
import org.labkey.api.view.ViewContext;
3536
import org.labkey.vfs.FileLike;
3637
import org.springframework.web.multipart.MultipartFile;
@@ -233,7 +234,7 @@ public Map<String, FileLike> savePostedFiles(ContextType context, @NotNull Set<S
233234
Set<String> originalFileNames = new HashSet<>();
234235
if (context.getRequest() instanceof MultipartHttpServletRequest multipartRequest)
235236
{
236-
Iterator<Map.Entry<String, List<MultipartFile>>> iter = multipartRequest.getMultiFileMap().entrySet().iterator();
237+
Iterator<Map.Entry<String, List<MultipartFile>>> iter = PageFlowUtil.getMultiFileMap(context.getRequest()).entrySet().iterator();
237238
Deque<FileLike> overflowFiles = new ArrayDeque<>(); // using a deque for easy removal of single elements
238239
Set<String> unusedParameterNames = new HashSet<>(parameterNames);
239240
while (iter.hasNext())

api/src/org/labkey/api/assay/actions/AssayRunUploadForm.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.apache.logging.log4j.Logger;
2424
import org.jetbrains.annotations.NotNull;
2525
import org.jetbrains.annotations.Nullable;
26+
import org.labkey.api.action.BaseViewAction;
2627
import org.labkey.api.assay.AbstractAssayProvider;
2728
import org.labkey.api.assay.AssayDataCollector;
2829
import org.labkey.api.assay.AssayFileWriter;
@@ -61,6 +62,7 @@
6162
import org.labkey.api.study.assay.ParticipantVisitResolverType;
6263
import org.labkey.api.util.FileUtil;
6364
import org.labkey.api.util.GUID;
65+
import org.labkey.api.util.PageFlowUtil;
6466
import org.labkey.api.view.ActionURL;
6567
import org.labkey.api.view.NotFoundException;
6668
import org.labkey.api.view.UnauthorizedException;
@@ -360,10 +362,11 @@ public Map<DomainProperty, FileLike> getAdditionalPostedFiles(List<? extends Dom
360362
File assayDirectory = getAssayDirectory(getContainer(), null);
361363

362364
// Hidden values in form containing previously uploaded files if the previous upload resulted in error
365+
var fileMap = PageFlowUtil.getFileMap(request);
363366
for (String fileParam : filePdNames)
364367
{
365368
DomainProperty domainProperty = fileParameters.get(fileParam);
366-
MultipartFile multiFile = request.getFileMap().get(fileParam);
369+
MultipartFile multiFile = fileMap.get(fileParam);
367370

368371
// If the file is removed from form after error, override hidden file name with an empty file
369372
if (null != multiFile && multiFile.getOriginalFilename().isEmpty())

api/src/org/labkey/api/attachments/AttachmentService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ static AttachmentService get()
140140

141141
HttpView<?> getFindAttachmentParentsView();
142142

143+
void logOrphanedAttachments();
144+
145+
void deleteOrphanedAttachments();
146+
143147
class DuplicateFilenameException extends IOException implements SkipMothershipLogging
144148
{
145149
private final List<String> _errors = new ArrayList<>();

api/src/org/labkey/api/audit/AuditHandler.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,6 @@ static Pair<Map<String, Object>, Map<String, Object>> getOldAndNewRecordForMerge
9191
if (col != null && (col.isMultiValued() || col.getFk() instanceof MultiValuedForeignKey))
9292
isMultiValued = true;
9393

94-
boolean isMultiChoice = col != null && col.getPropertyType() == PropertyType.MULTI_CHOICE;
95-
9694
String nameFromAlias = key;
9795
if (null != col)
9896
nameFromAlias = col.getName();
@@ -104,9 +102,13 @@ static Pair<Map<String, Object>, Map<String, Object>> getOldAndNewRecordForMerge
104102
{
105103
if (aliasColumn.getFk() != null && (aliasColumn.isMultiValued() || aliasColumn.getFk() instanceof MultiValuedForeignKey))
106104
isMultiValued = true;
105+
col = aliasColumn; // GitHub Issue 913: Updating a sample details page shows an update to the MVTC field
107106
nameFromAlias = aliasColumn.getName();
108107
}
109108
}
109+
110+
boolean isMultiChoice = col != null && col.getPropertyType() == PropertyType.MULTI_CHOICE;
111+
110112
String lcName = nameFromAlias.toLowerCase();
111113
// Preserve casing of inputs so we can show the names properly
112114
boolean isExpInput = false; // TODO: extract lineage handling out of this generic method

api/src/org/labkey/api/data/CompareType.java

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,10 @@ public Pair<SQLFragment, SQLFragment> getSqlFragments(Map<FieldKey, ? extends Co
10031003
return null;
10041004

10051005
ColumnInfo colInfo = columnMap != null ? columnMap.get(_fieldKey) : null;
1006+
1007+
if (colInfo != null && colInfo.getJdbcType() != JdbcType.ARRAY)
1008+
throw new RuntimeSQLException(new SQLGenerationException("Invalid filter type for column '" + _fieldKey.toDisplayString() + "'."));
1009+
10061010
var alias = SimpleFilter.getAliasForColumnFilter(dialect, colInfo, _fieldKey);
10071011

10081012
SQLFragment valuesFragment = dialect.array_construct(paramValues);
@@ -1040,6 +1044,10 @@ public ArrayIsEmptyClause(@NotNull FieldKey fieldKey)
10401044
public SQLFragment toSQLFragment(Map<FieldKey, ? extends ColumnInfo> columnMap, SqlDialect dialect)
10411045
{
10421046
ColumnInfo colInfo = columnMap != null ? columnMap.get(_fieldKey) : null;
1047+
1048+
if (colInfo != null && colInfo.getJdbcType() != JdbcType.ARRAY)
1049+
throw new RuntimeSQLException(new SQLGenerationException("Invalid filter type for column '" + _fieldKey.toDisplayString() + "'."));
1050+
10431051
var alias = SimpleFilter.getAliasForColumnFilter(dialect, colInfo, _fieldKey);
10441052

10451053
SQLFragment columnFragment = new SQLFragment().appendIdentifier(alias);
@@ -1426,30 +1434,30 @@ protected static void parseParams(String value, String separator, Collection<Str
14261434
// check for JSON marker
14271435
if (value.startsWith(JSON_MARKER_START) && value.endsWith(JSON_MARKER_END))
14281436
{
1429-
value = value.substring(JSON_MARKER_START.length(), value.length()-JSON_MARKER_END.length());
1430-
if (value.startsWith("[") && value.endsWith("]"))
1437+
String cleanedValue = value.substring(JSON_MARKER_START.length(), value.length()-JSON_MARKER_END.length());
1438+
if (cleanedValue.startsWith("[") && cleanedValue.endsWith("]"))
14311439
{
14321440
try
14331441
{
1434-
// TODO what do we do with malformed parameters???
1435-
JSONArray array = new JSONArray(value);
1442+
JSONArray array = new JSONArray(cleanedValue);
14361443
for (int i = 0; i < array.length(); i++)
14371444
{
14381445
Object jsonVal = array.get(i);
14391446
collection.add(JSONObject.NULL.equals(jsonVal) ? null : Objects.toString(jsonVal));
14401447
}
1448+
return;
14411449
}
14421450
catch (JSONException ex)
14431451
{
1444-
// pass
1452+
// GH Issue #948
1453+
// Intentionally do nothing, so we revert to parsing by regex. Otherwise, valid filters like an IN
1454+
// filter on "{json:123;abc}" will be skipped instead of parsed as ["{json:123", "abc}"]
14451455
}
14461456
}
14471457
}
1448-
else
1449-
{
1450-
String[] st = value.split("\\s*" + separator + "\\s*", -1);
1451-
Collections.addAll(collection, st);
1452-
}
1458+
1459+
String[] st = value.split("\\s*" + separator + "\\s*", -1);
1460+
Collections.addAll(collection, st);
14531461
}
14541462

14551463
protected static Set<String> parseParams(Object value_, String separator)
@@ -1747,6 +1755,9 @@ protected String toURLParamValue()
17471755
public SQLFragment toSQLFragment(Map<FieldKey, ? extends ColumnInfo> columnMap, SqlDialect dialect)
17481756
{
17491757
ColumnInfo colInfo = columnMap != null ? columnMap.get(_fieldKey) : null;
1758+
if (colInfo != null && colInfo.getJdbcType() == JdbcType.ARRAY)
1759+
throw new RuntimeSQLException(new SQLGenerationException("Invalid filter type for column '" + _fieldKey.toDisplayString() + "'."));
1760+
17501761
var alias = SimpleFilter.getAliasForColumnFilter(dialect, colInfo, _fieldKey);
17511762

17521763
SQLFragment fragment = toWhereClause(dialect, alias);

0 commit comments

Comments
 (0)