Skip to content

Commit

Permalink
Decode extended filename in multipart content-disposition
Browse files Browse the repository at this point in the history
Signed-off-by: jansupol <jan.supol@oracle.com>
  • Loading branch information
jansupol authored and senivam committed Oct 12, 2023
1 parent dbbf057 commit 4f2b834
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 29 deletions.
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

0 comments on commit 4f2b834

Please sign in to comment.