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
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.ref.Cleaner;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
Expand Down Expand Up @@ -68,6 +69,8 @@ public class HttpRequestWrapper implements RequestWrapper {
// to handlers not written to expect a body.
private static final Set<String> METHODS_WITH_REQUEST_BODY = Set.of("POST", "PUT", "PATCH");

private static final Cleaner CLEANER = Cleaner.create();

private static final Path TMP_DIR;
static {
try {
Expand All @@ -93,7 +96,7 @@ public class HttpRequestWrapper implements RequestWrapper {
// flag to indicate whether multipart form data was processed
private final boolean isFormDataParsed;

@Nullable private Map<Part, Path> temporaryUploadedFilesPathCache = null;
private final Map<Part, Path> temporaryUploadedFilesPathCache = new HashMap<>();

private boolean parsedQueryString = false;
@Nullable private Map<String, Object> queryStringParameters = null;
Expand Down Expand Up @@ -170,6 +173,10 @@ public HttpRequestWrapper(final HttpServletRequest servletRequest,
}

LOG.debug("Retrieved {} parameters.", params.size());

// Delete any temporary files created for uploaded file parts once this wrapper is
// unreachable. The cleaning action holds only the map, never `this`.
CLEANER.register(this, new TemporaryUploadedFilesCleaner(temporaryUploadedFilesPathCache));
}

@Override
Expand Down Expand Up @@ -554,10 +561,7 @@ public List<Path> getFileUploadParam(final String name) {
final List<Part> parts = getFileItem(o);
final List<Path> files = new ArrayList<>(parts.size());
for (final Part part : parts) {
Path temporaryUploadedFilePath = null;
if (temporaryUploadedFilesPathCache != null) {
temporaryUploadedFilePath = temporaryUploadedFilesPathCache.get(part);
}
Path temporaryUploadedFilePath = temporaryUploadedFilesPathCache.get(part);

if (temporaryUploadedFilePath == null) {
try {
Expand All @@ -570,10 +574,6 @@ public List<Path> getFileUploadParam(final String name) {
continue;
}

if (temporaryUploadedFilesPathCache == null) {
temporaryUploadedFilesPathCache = new HashMap<>();
}

temporaryUploadedFilesPathCache.put(part, temporaryUploadedFilePath);
}

Expand Down Expand Up @@ -604,6 +604,29 @@ public List<String> getUploadedFileName(final String name) {
return files;
}

@Override
public List<Map<String, String>> getUploadedFileHeaders(final String name) {
if (!isFormDataParsed) {
return null;
}

final Object o = params.get(name);
if (o == null) {
return null;
}

final List<Part> parts = getFileItem(o);
final List<Map<String, String>> fileHeaders = new ArrayList<>(parts.size());
for (final Part part : parts) {
final Map<String, String> headers = new LinkedHashMap<>();
for (final String headerName : part.getHeaderNames()) {
headers.put(headerName, part.getHeader(headerName));
}
fileHeaders.add(headers);
}
return fileHeaders;
}

/**
* @see jakarta.servlet.http.HttpServletRequest#getParameterNames()
*/
Expand Down Expand Up @@ -938,22 +961,25 @@ public RequestDispatcher getRequestDispatcher(final String path) {
return servletRequest.getRequestDispatcher(path);
}

@Override
protected void finalize() {
if (temporaryUploadedFilesPathCache == null) {
return;
}
/**
* Deletes the temporary files created for uploaded file parts once the owning request
* wrapper is no longer reachable. Registered with a {@link Cleaner}; holds only the map of
* temporary files (never the wrapper), so it does not keep the wrapper alive.
*/
private record TemporaryUploadedFilesCleaner(Map<Part, Path> temporaryUploadedFiles) implements Runnable {

for (final Map.Entry<Part, Path> temporaryUploadedFilePathCache : temporaryUploadedFilesPathCache.entrySet()) {
final Part part = temporaryUploadedFilePathCache.getKey();
try {
part.delete();
} catch (final IOException e) {
LOG.error("Unable to delete: {}", part.getSubmittedFileName(), e);
}
@Override
public void run() {
for (final Map.Entry<Part, Path> entry : temporaryUploadedFiles.entrySet()) {
final Part part = entry.getKey();
try {
part.delete();
} catch (final IOException e) {
LOG.error("Unable to delete: {}", part.getSubmittedFileName(), e);
}

final Path temporaryFile = temporaryUploadedFilePathCache.getValue();
FileUtils.deleteQuietly(temporaryFile);
FileUtils.deleteQuietly(entry.getValue());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.security.Principal;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;

public interface RequestWrapper {

Expand Down Expand Up @@ -578,4 +579,18 @@ public interface RequestWrapper {
* @return the list of file names.
*/
List<String> getUploadedFileName(String name);

/**
* Get the part headers of each uploaded file submitted under a parameter name.
*
* There is one map per uploaded file (in submission order), aligned with the entries
* returned by {@link #getUploadedFileName(String)} and {@link #getFileUploadParam(String)}.
* Each map holds the file part's own headers (for example {@code Content-Type} and
* {@code Content-Disposition}), keyed by header name as submitted.
*
* @param name the parameter name
*
* @return the list of header maps, one per uploaded file.
*/
List<Map<String, String>> getUploadedFileHeaders(String name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
* info@exist-db.org
* http://www.exist-db.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.exist.xquery.functions.request;

import java.util.List;
import java.util.Map;

import org.exist.dom.QName;
import org.exist.http.servlets.RequestWrapper;
import org.exist.xquery.Cardinality;
import org.exist.xquery.FunctionSignature;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQueryContext;
import org.exist.xquery.functions.map.MapType;
import org.exist.xquery.value.FunctionParameterSequenceType;
import org.exist.xquery.value.FunctionReturnSequenceType;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.SequenceType;
import org.exist.xquery.value.StringValue;
import org.exist.xquery.value.Type;
import org.exist.xquery.value.ValueSequence;

import javax.annotation.Nonnull;

/**
* Retrieve the part headers of each uploaded file in a multi-part request.
*/
public class GetUploadedFileHeaders extends StrictRequestFunction {

public final static FunctionSignature signature =
new FunctionSignature(
new QName("get-uploaded-file-headers", RequestModule.NAMESPACE_URI, RequestModule.PREFIX),
"Retrieve the part headers of each uploaded file submitted under a parameter name in a " +
"multi-part request. Returns one map (header name to header value) per uploaded file, in " +
"submission order and aligned with request:get-uploaded-file-name. Header names are keyed " +
"as submitted. Returns the empty sequence if the request is not a multi-part request or the " +
"parameter name does not point to a file part.",
new SequenceType[] {
new FunctionParameterSequenceType("upload-param-name", Type.STRING, Cardinality.EXACTLY_ONE, "The parameter name")
},
new FunctionReturnSequenceType(Type.MAP_ITEM, Cardinality.ZERO_OR_MORE, "one map of header name to header value per uploaded file"));

public GetUploadedFileHeaders(final XQueryContext context) {
super(context, signature);
}

@Override
public Sequence eval(final Sequence[] args, @Nonnull final RequestWrapper request)
throws XPathException {
final String uploadParamName = args[0].getStringValue();
final List<Map<String, String>> fileHeaders = request.getUploadedFileHeaders(uploadParamName);
if (fileHeaders == null || fileHeaders.isEmpty()) {
return Sequence.EMPTY_SEQUENCE;
}

final ValueSequence result = new ValueSequence();
for (final Map<String, String> headers : fileHeaders) {
final MapType map = new MapType(this, context);
for (final Map.Entry<String, String> header : headers.entrySet()) {
map.add(new StringValue(this, header.getKey()), new StringValue(this, header.getValue()));
}
result.add(map);
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public class RequestModule extends AbstractInternalModule {
new FunctionDef(GetQueryString.signature, GetQueryString.class),
new FunctionDef(GetUploadedFile.signatures[0], GetUploadedFile.class),
new FunctionDef(GetUploadedFileName.signature, GetUploadedFileName.class),
new FunctionDef(GetUploadedFileHeaders.signature, GetUploadedFileHeaders.class),
new FunctionDef(GetUploadedFileSize.signature, GetUploadedFileSize.class),
new FunctionDef(GetURI.signatures[0], GetURI.class),
new FunctionDef(GetURI.signatures[1], GetURI.class),
Expand Down
Loading
Loading