Skip to content

Commit

Permalink
Add support RFC 5987 for attribute filename* in HTTP header Content-D…
Browse files Browse the repository at this point in the history
…isposition (#4647)

Add support RFC 5987 for attribute filename* in HTTP header Content-Disposition

Signed-off-by: Andrii Serkes <[email protected]>
  • Loading branch information
aserkes authored Jan 14, 2021
1 parent bf95038 commit 830db2e
Show file tree
Hide file tree
Showing 2 changed files with 200 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2019 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2010, 2021 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 @@ -20,9 +20,12 @@
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

/**
* A content disposition header.
Expand All @@ -41,6 +44,18 @@ public class ContentDisposition {
private Date readDate;
private long size;

private static final String CHARSET_GROUP_NAME = "charset";
private static final String CHARSET_REGEX = "(?<" + CHARSET_GROUP_NAME + ">[^']+)";
private static final String LANG_GROUP_NAME = "lang";
private static final String LANG_REGEX = "(?<" + LANG_GROUP_NAME + ">[a-z]{2,8}(-[a-z0-9-]+)?)?";
private static final String FILENAME_GROUP_NAME = "filename";
private static final String FILENAME_REGEX = "(?<" + FILENAME_GROUP_NAME + ">.+)";
private static final Pattern FILENAME_EXT_VALUE_PATTERN =
Pattern.compile(CHARSET_REGEX + "'" + LANG_REGEX + "'" + FILENAME_REGEX,
Pattern.CASE_INSENSITIVE);
private static final Pattern FILENAME_VALUE_CHARS_PATTERN =
Pattern.compile("(%[a-f0-9]{2}|[a-z0-9!#$&+.^_`|~-])+", Pattern.CASE_INSENSITIVE);

protected ContentDisposition(final String type, final String fileName, final Date creationDate,
final Date modificationDate, final Date readDate, final long size) {
this.type = type;
Expand Down Expand Up @@ -181,7 +196,7 @@ protected void addLongParameter(final StringBuilder sb, final String name, final
}

private void createParameters() throws ParseException {
fileName = parameters.get("filename");
fileName = defineFileName();

creationDate = createDate("creation-date");

Expand All @@ -192,6 +207,49 @@ private void createParameters() throws ParseException {
size = createLong("size");
}

private String defineFileName() throws ParseException {

final String fileName = parameters.get("filename");
final String fileNameExt = parameters.get("filename*");

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

final Matcher matcher = FILENAME_EXT_VALUE_PATTERN.matcher(fileNameExt);

if (matcher.matches()) {

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();
} else {
throw new ParseException(charset + " charset is not supported", 0);
}
}

throw new ParseException(fileNameExt + " - unsupported filename parameter", 0);
}

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

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

private Date createDate(final String name) throws ParseException {
final String value = parameters.get(name);
if (value == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2021 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 @@ -24,10 +24,12 @@
import org.glassfish.jersey.message.internal.HttpHeaderReader;

import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;


/**
* @author [email protected]
*/
Expand Down Expand Up @@ -97,6 +99,143 @@ public void testToString() {
assertEquals(header, contentDisposition.toString());
}

@Test
public void testFileNameExt() {
final String fileName = "test.file";
String fileNameExt;
String encodedFilename;
try {
//incorrect fileNameExt - does not contain charset''
try {
fileNameExt = "testExt.file";
assertFileNameExt(fileName, fileName, fileNameExt);
fail("ParseException was expected to be thrown.");
} catch (ParseException e) {
//expected
}

//correct fileNameExt
fileNameExt = "ISO-8859-1'language-us'abc%a1abc%a2%b1!#$&+.^_`|~-";
assertFileNameExt(fileNameExt, fileName, fileNameExt);

//correct fileNameExt
fileNameExt = "UTF-8'language-us'abc%a1abc%a2%b1!#$&+.^_`|~-";
assertFileNameExt(fileNameExt, fileName, fileNameExt);

//correct fileNameExt
fileNameExt = "UTF-8'us'fileName.txt";
assertFileNameExt(fileNameExt, fileName, fileNameExt);

//incorrect fileNameExt - too long language tag
try {
fileNameExt = "utf-8'languageTooLong'fileName.txt";
assertFileNameExt(fileName, fileName, fileNameExt);
fail("ParseException was expected to be thrown.");
} catch (ParseException e) {
//expected
}

//correct fileNameExt
fileNameExt = "utf-8''a";
assertFileNameExt(fileNameExt, fileName, fileNameExt);

//incorrect fileNameExt - language tag does not match to pattern
try {
fileNameExt = "utf-8'lang-'a";
assertFileNameExt(fileName, fileName, fileNameExt);
fail("ParseException was expected to be thrown.");
} catch (ParseException e) {
//expected
}

//incorrect fileNameExt - ext-value contains an inappropriate symbol sequence (%z1). Jersey encodes it.
fileNameExt = "utf-8'language-us'a%z1";
encodedFilename = "utf-8'language-us'a%25z1";
assertFileNameExt(encodedFilename, fileName, fileNameExt);

//Incorrect fileNameExt - ext-value contains an inappropriate symbol sequence (%z1).
//Jersey won't encodes it because of the unsupported charset.
try {
fileNameExt = "windows-1251'ru-ru'a%z1";
assertFileNameExt(fileName, fileName, fileNameExt);
fail("ParseException was expected to be thrown.");
} catch (ParseException e) {
//expected
}

//correct fileNameExt
fileNameExt = "UTF-8'language-us'abc%a1abc%a2%b1";
assertFileNameExt(fileNameExt, fileName, fileNameExt);

//correct fileNameExt - encoded with other charset
fileNameExt = "UTF-16'language-us'abc%a1abc%a2%b1";
assertFileNameExt(fileNameExt, fileName, fileNameExt);

//correct fileNameExt - unsupported charset, but fileName contains only valid characters
fileNameExt = "Windows-1251'sr-Latn-RS'abc";
assertFileNameExt(fileNameExt, fileName, fileNameExt);

//correct fileNameExt
fileNameExt = "utf-8'sr-Latn-RS'a";
assertFileNameExt(fileNameExt, fileName, fileNameExt);

//incorrect fileNameExt - ext-value contains % without two HEXDIG. Jersey encodes it.
fileNameExt = "utf-8'language-us'a%";
encodedFilename = "utf-8'language-us'a%25";
assertFileNameExt(encodedFilename, fileName, fileNameExt);

//correct fileNameExt
fileNameExt = "UTF-8'language-us'abc.TXT";
assertFileNameExt(fileNameExt, fileName, fileNameExt);

//incorrect fileNameExt - no ext-value
try {
fileNameExt = "utf-8'language-us'";
assertFileNameExt(fileName, fileName, fileNameExt);
fail("ParseException was expected to be thrown.");
} catch (ParseException e) {
//expected
}

//incorrect fileNameExt - ext-value contains forbidden symbol (\). Jersey encodes it.
fileNameExt = "utf-8'language-us'c:\\\\file.txt";
encodedFilename = "utf-8'language-us'c%3A%5Cfile.txt";
assertFileNameExt(encodedFilename, fileName, fileNameExt);

//incorrect fileNameExt - ext-value contains forbidden symbol (/). Jersey encodes it.
fileNameExt = "utf-8'language-us'home/file.txt";
encodedFilename = "utf-8'language-us'home%2Ffile.txt";
assertFileNameExt(encodedFilename, fileName, fileNameExt);

//incorrect fileNameExt - ext-value contains forbidden symbol (李). Jersey encodes it.
fileNameExt = "utf-8'language-us'李.txt";
encodedFilename = "utf-8'language-us'%E6%9D%8E.txt";
assertFileNameExt(encodedFilename, fileName, fileNameExt);

//correct fileNameExt
fileNameExt = "utf-8'language-us'FILEname.tXt";
assertFileNameExt(fileNameExt, fileName, fileNameExt);

} catch (ParseException ex) {
fail(ex.getMessage());
}
}

private void assertFileNameExt(
final String expectedFileName,
final String actualFileName,
final String actualFileNameExt
) throws ParseException {
final Date date = new Date();
final String dateString = HttpDateFormat.getPreferredDateFormat().format(date);
final String prefixHeader = contentDispositionType + ";filename=\"" + actualFileName + "\";"
+ "creation-date=\"" + dateString + "\";modification-date=\"" + dateString + "\";read-date=\""
+ dateString + "\";size=1222" + ";name=\"testData\";" + "filename*=\"";
final String header = prefixHeader + actualFileNameExt + "\"";
final ContentDisposition contentDisposition = new ContentDisposition(HttpHeaderReader.newInstance(header), true);
assertEquals(expectedFileName, contentDisposition.getFileName());
}

protected void assertContentDisposition(final ContentDisposition contentDisposition, Date date) {
assertNotNull(contentDisposition);
assertEquals(contentDispositionType, contentDisposition.getType());
Expand Down

0 comments on commit 830db2e

Please sign in to comment.