diff --git a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/ContentDisposition.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/ContentDisposition.java index ab9e14512f..3d71203ef8 100644 --- a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/ContentDisposition.java +++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/ContentDisposition.java @@ -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 @@ -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. @@ -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; @@ -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"); @@ -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) { diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ContentDispositionTest.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ContentDispositionTest.java index 63f2cd2eee..8cbdcc1a50 100644 --- a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ContentDispositionTest.java +++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ContentDispositionTest.java @@ -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 @@ -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 Imran@SmartITEngineering.Com */ @@ -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());