Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decode extended filename in multipart content-disposition #5432

Merged
merged 1 commit into from
Oct 12, 2023
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
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2021 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
Expand All @@ -23,10 +23,13 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.glassfish.jersey.media.multipart.internal.LocalizationMessages;
import org.glassfish.jersey.message.internal.HttpDateFormat;
import org.glassfish.jersey.message.internal.HttpHeaderReader;
import org.glassfish.jersey.uri.UriComponent;

import javax.ws.rs.core.HttpHeaders;

/**
* A content disposition header.
*
Expand All @@ -43,6 +46,7 @@ public class ContentDisposition {
private Date modificationDate;
private Date readDate;
private long size;
private boolean encoded; // received encoded by filename*=

private static final String CHARSET_GROUP_NAME = "charset";
private static final String CHARSET_REGEX = "(?<" + CHARSET_GROUP_NAME + ">[^']+)";
Expand All @@ -65,6 +69,7 @@ protected ContentDisposition(final String type, final String fileName, final Dat
this.readDate = readDate;
this.size = size;
this.parameters = Collections.emptyMap();
this.encoded = false;
}

public ContentDisposition(final String header) throws ParseException {
Expand Down Expand Up @@ -110,12 +115,23 @@ public Map<String, String> getParameters() {
}

/**
* Get the filename parameter.
* Get the filename parameter. Automatically decodes RFC 5987 extended filename*= to be human-readable.
*
* @return the size
* @return the file name
*/
public String getFileName() {
return fileName;
return getFileName(true);
}

/**
* Get the filename parameter. If the RFC 5987 extended filename*= is received in Content-Disposition, its encoded
* value can be decoded to be human-readable.
*
* @param decodeExtended decode the filename* to be human-readable when {@code true}
* @return the filename or the RFC 5987 extended filename
*/
public String getFileName(boolean decodeExtended) {
return encoded && decodeExtended ? decodeFromUriFormat(fileName) : fileName;
}

/**
Expand Down Expand Up @@ -196,7 +212,7 @@ protected void addLongParameter(final StringBuilder sb, final String name, final
}

private void createParameters() throws ParseException {
fileName = defineFileName();
defineFileName();

creationDate = createDate("creation-date");

Expand All @@ -207,46 +223,59 @@ private void createParameters() throws ParseException {
size = createLong("size");
}

private String defineFileName() throws ParseException {

private void defineFileName() throws ParseException {
encoded = false;
final String fileName = parameters.get("filename");
final String fileNameExt = parameters.get("filename*");

if (fileNameExt == null) {
return fileName;
this.fileName = fileName;
return;
}

final Matcher matcher = FILENAME_EXT_VALUE_PATTERN.matcher(fileNameExt);

if (matcher.matches()) {
encoded = true;

final String fileNameValueChars = matcher.group(FILENAME_GROUP_NAME);
if (isFilenameValueCharsEncoded(fileNameValueChars)) {
return fileNameExt;
}

final String charset = matcher.group(CHARSET_GROUP_NAME);
if (matcher.group(CHARSET_GROUP_NAME).equalsIgnoreCase("UTF-8")) {
final String language = matcher.group(LANG_GROUP_NAME);
return new StringBuilder(charset)
.append("'")
.append(language == null ? "" : language)
.append("'")
.append(encodeToUriFormat(fileNameValueChars))
.toString();
this.fileName = fileNameExt;
} else {
throw new ParseException(charset + " charset is not supported", 0);

final String charset = matcher.group(CHARSET_GROUP_NAME);
if (charset.equalsIgnoreCase("UTF-8")) {
final String language = matcher.group(LANG_GROUP_NAME);
this.fileName = new StringBuilder(charset)
.append("'")
.append(language == null ? "" : language)
.append("'")
.append(encodeToUriFormat(fileNameValueChars))
.toString();
} else {
throw new ParseException(LocalizationMessages.ERROR_CHARSET_UNSUPPORTED(charset), 0);
}
}
} else {
throw new ParseException(LocalizationMessages.ERROR_FILENAME_UNSUPPORTED(fileNameExt), 0);
}
}

throw new ParseException(fileNameExt + " - unsupported filename parameter", 0);
private static String decodeFromUriFormat(String parameter) {
final Matcher matcher = FILENAME_EXT_VALUE_PATTERN.matcher(parameter);
if (matcher.matches()) {
final String fileNameValueChars = matcher.group(FILENAME_GROUP_NAME);
return UriComponent.decode(fileNameValueChars, UriComponent.Type.UNRESERVED);
} else {
return parameter;
}
}

private String encodeToUriFormat(final String parameter) {
private static String encodeToUriFormat(final String parameter) {
return UriComponent.contextualEncode(parameter, UriComponent.Type.UNRESERVED);
}

private boolean isFilenameValueCharsEncoded(final String parameter) {
private static boolean isFilenameValueCharsEncoded(final String parameter) {
return FILENAME_VALUE_CHARS_PATTERN.matcher(parameter).matches();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
# Copyright (c) 2012, 2023 Oracle and/or its affiliates. All rights reserved.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License v. 2.0, which is available at
Expand All @@ -16,6 +16,8 @@

cannot.inject.file=Cannot provide file for an entity body part.
entity.has.wrong.type=Entity instance does not contain the unconverted content.
error.charset.unsupported={0} charset is not supported.
error.filename.unsupported=Unsupported filename parameter {0}.
error.parsing.content.disposition=Error parsing content disposition: {0}
error.reading.entity=Error reading entity as {0}.
form.data.multipart.cannot.change.mediatype=Cannot change media type of a FormDataMultiPart instance.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2022 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2023 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
Expand Down Expand Up @@ -102,7 +102,7 @@ public void testToString() {
@Test
public void testFileNameExt() {
final String fileName = "test.file";
String fileNameExt;
String fileNameExt = null;
String encodedFilename;
try {
//incorrect fileNameExt - does not contain charset''
Expand Down Expand Up @@ -217,14 +217,31 @@ public void testFileNameExt() {
assertFileNameExt(fileNameExt, fileName, fileNameExt);

} catch (ParseException ex) {
fail(ex.getMessage());
fail(ex.getMessage() + " for " + fileNameExt);
}
}

@Test
void testDecoding() throws ParseException {
final String fileName = "Ueberflieger.jpg";
final String extendedFilename = "UTF-8'de'%C3%9Cberflieger.jpg";
assertFileNameExt("Überflieger.jpg", fileName, extendedFilename, true);
}


private void assertFileNameExt(
final String expectedFileName,
final String actualFileName,
final String actualFileNameExt
) throws ParseException {
assertFileNameExt(expectedFileName, actualFileName, actualFileNameExt, false);
}

private void assertFileNameExt(
final String expectedFileName,
final String actualFileName,
final String actualFileNameExt,
final boolean decode
) throws ParseException {
final Date date = new Date();
final String dateString = HttpDateFormat.getPreferredDateFormat().format(date);
Expand All @@ -233,7 +250,7 @@ private void assertFileNameExt(
+ dateString + "\";size=1222" + ";name=\"testData\";" + "filename*=\"";
final String header = prefixHeader + actualFileNameExt + "\"";
final ContentDisposition contentDisposition = new ContentDisposition(HttpHeaderReader.newInstance(header), true);
assertEquals(expectedFileName, contentDisposition.getFileName());
assertEquals(expectedFileName, contentDisposition.getFileName(decode));
}

protected void assertContentDisposition(final ContentDisposition contentDisposition, Date date) {
Expand Down