From 94df5a7ed3bd02f08d4171a832ce3a29ccf1039b Mon Sep 17 00:00:00 2001 From: =?utf8?q?Leif=20=C3=85strand?= Date: Tue, 13 Dec 2016 11:28:55 +0200 Subject: [PATCH] Use proper UTF-8 encoding for Content-Disposition filenames (#19527) (#6607) --- .../com/vaadin/server/DownloadStream.java | 15 ++-- .../main/java/com/vaadin/util/EncodeUtil.java | 71 ++++++++++++++++++ .../com/vaadin/server/DownloadStreamTest.java | 16 ++-- .../tests/server/ClassesSerializableTest.java | 13 ++-- .../java/com/vaadin/util/EncodeUtilTest.java | 32 ++++++++ .../tests/components/FileDownloaderUI.java | 4 +- ...-\346\227\245\346\234\254\350\252\236.pdf" | Bin 7 files changed, 128 insertions(+), 23 deletions(-) create mode 100644 server/src/main/java/com/vaadin/util/EncodeUtil.java create mode 100644 server/src/test/java/com/vaadin/util/EncodeUtilTest.java rename "uitest/src/main/resources/com/vaadin/tests/components/embedded/\303\245\303\244\303\266-\346\227\245\346\234\254\350\252\236.pdf" => "uitest/src/main/resources/com/vaadin/tests/components/embedded/File \303\245\303\244\303\266-\346\227\245\346\234\254\350\252\236.pdf" (100%) diff --git a/server/src/main/java/com/vaadin/server/DownloadStream.java b/server/src/main/java/com/vaadin/server/DownloadStream.java index fdc757bd5e..c13cad0018 100644 --- a/server/src/main/java/com/vaadin/server/DownloadStream.java +++ b/server/src/main/java/com/vaadin/server/DownloadStream.java @@ -20,14 +20,14 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.servlet.http.HttpServletResponse; +import com.vaadin.util.EncodeUtil; + /** * Downloadable stream. *

@@ -329,13 +329,10 @@ public class DownloadStream implements Serializable { * @return A value for inclusion in a Content-Disposition header */ public static String getContentDispositionFilename(String filename) { - try { - String encodedFilename = URLEncoder.encode(filename, "UTF-8"); - return String.format("filename=\"%s\"; filename*=utf-8''%s", - encodedFilename, encodedFilename); - } catch (UnsupportedEncodingException e) { - return null; - } + String encodedFilename = EncodeUtil.rfc5987Encode(filename); + + return String.format("filename=\"%s\"; filename*=utf-8''%s", + encodedFilename, encodedFilename); } /** diff --git a/server/src/main/java/com/vaadin/util/EncodeUtil.java b/server/src/main/java/com/vaadin/util/EncodeUtil.java new file mode 100644 index 0000000000..3319211c2c --- /dev/null +++ b/server/src/main/java/com/vaadin/util/EncodeUtil.java @@ -0,0 +1,71 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.util; + +import java.nio.charset.Charset; + +/** + * Utilities related to various encoding schemes. + * + * @author Vaadin Ltd + * @since + */ +public class EncodeUtil { + private static final Charset utf8 = Charset.forName("UTF-8"); + + private EncodeUtil() { + // Static utils only + } + + /** + * Encodes the given string to UTF-8 value-chars as defined in + * RFC5987 for use in e.g. the Content-Disposition HTTP header. + * + * @param value + * the string to encode, not null + * @return the encoded string + */ + public static String rfc5987Encode(String value) { + StringBuilder builder = new StringBuilder(); + + for (int i = 0; i < value.length();) { + int cp = value.codePointAt(i); + if (cp < 127 && (Character.isLetterOrDigit(cp) || cp == '.')) { + builder.append((char) cp); + } else { + // Create string from a single code point + String cpAsString = new String(new int[] { cp }, 0, 1); + + appendHexBytes(builder, cpAsString.getBytes(utf8)); + } + + // Advance to the next code point + i += Character.charCount(cp); + } + + return builder.toString(); + } + + private static void appendHexBytes(StringBuilder builder, byte[] bytes) { + for (byte byteValue : bytes) { + // mask with 0xFF to compensate for "negative" values + int intValue = byteValue & 0xFF; + String hexCode = Integer.toString(intValue, 16); + builder.append('%').append(hexCode); + } + } + +} diff --git a/server/src/test/java/com/vaadin/server/DownloadStreamTest.java b/server/src/test/java/com/vaadin/server/DownloadStreamTest.java index f302163ef7..79347a3f3d 100644 --- a/server/src/test/java/com/vaadin/server/DownloadStreamTest.java +++ b/server/src/test/java/com/vaadin/server/DownloadStreamTest.java @@ -7,13 +7,16 @@ import static org.mockito.Mockito.verify; import java.io.IOException; import java.io.InputStream; -import java.net.URLEncoder; import org.junit.Before; import org.junit.Test; public class DownloadStreamTest { - private String filename = "日本語.png"; + private String filename = "A å日.png"; + private String encodedFileName = "A" + "%20" // space + + "%c3%a5" // å + + "%e6%97%a5" // 日 + + ".png"; private DownloadStream stream; @Before @@ -27,11 +30,12 @@ public class DownloadStreamTest { stream.writeResponse(mock(VaadinRequest.class), response); - String encodedFileName = URLEncoder.encode(filename, "utf-8"); verify(response).setHeader(eq(DownloadStream.CONTENT_DISPOSITION), contains(String.format("filename=\"%s\";", encodedFileName))); - verify(response).setHeader(eq(DownloadStream.CONTENT_DISPOSITION), - contains( - String.format("filename*=utf-8''%s", encodedFileName))); + verify(response) + .setHeader( + eq(DownloadStream.CONTENT_DISPOSITION), + contains(String.format("filename*=utf-8''%s", + encodedFileName))); } } diff --git a/server/src/test/java/com/vaadin/tests/server/ClassesSerializableTest.java b/server/src/test/java/com/vaadin/tests/server/ClassesSerializableTest.java index 28bf497afe..f32af8a3bc 100644 --- a/server/src/test/java/com/vaadin/tests/server/ClassesSerializableTest.java +++ b/server/src/test/java/com/vaadin/tests/server/ClassesSerializableTest.java @@ -27,7 +27,8 @@ public class ClassesSerializableTest { private static String[] BASE_PACKAGES = { "com.vaadin" }; - private static String[] EXCLUDED_PATTERNS = { "com\\.vaadin\\.demo\\..*", // + private static String[] EXCLUDED_PATTERNS = { + "com\\.vaadin\\.demo\\..*", // "com\\.vaadin\\.external\\.org\\.apache\\.commons\\.fileupload\\..*", // "com\\.vaadin\\.launcher\\..*", // "com\\.vaadin\\.client\\..*", // @@ -58,6 +59,7 @@ public class ClassesSerializableTest { // interfaces "com\\.vaadin\\.server\\.LegacyCommunicationManager.*", // "com\\.vaadin\\.buildhelpers.*", // + "com\\.vaadin\\.util\\.EncodeUtil.*", // "com\\.vaadin\\.util\\.ReflectTools.*", // "com\\.vaadin\\.data\\.util\\.ReflectTools.*", // "com\\.vaadin\\.data\\.util.BeanItemContainerGenerator.*", @@ -148,9 +150,8 @@ public class ClassesSerializableTest { nonSerializableString += ")"; } } - Assert.fail( - "Serializable not implemented by the following classes and interfaces: " - + nonSerializableString); + Assert.fail("Serializable not implemented by the following classes and interfaces: " + + nonSerializableString); } } @@ -272,8 +273,8 @@ public class ClassesSerializableTest { while (e.hasMoreElements()) { JarEntry entry = e.nextElement(); if (entry.getName().endsWith(".class")) { - String nameWithoutExtension = entry.getName() - .replaceAll("\\.class", ""); + String nameWithoutExtension = entry.getName().replaceAll( + "\\.class", ""); String className = nameWithoutExtension.replace('/', '.'); classes.add(className); } diff --git a/server/src/test/java/com/vaadin/util/EncodeUtilTest.java b/server/src/test/java/com/vaadin/util/EncodeUtilTest.java new file mode 100644 index 0000000000..7bac539968 --- /dev/null +++ b/server/src/test/java/com/vaadin/util/EncodeUtilTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.util; + +import org.junit.Assert; +import org.junit.Test; + +public class EncodeUtilTest { + @Test + public void rfc5987Encode() { + Assert.assertEquals("A", EncodeUtil.rfc5987Encode("A")); + Assert.assertEquals("%20", EncodeUtil.rfc5987Encode(" ")); + Assert.assertEquals("%c3%a5", EncodeUtil.rfc5987Encode("å")); + Assert.assertEquals("%e6%97%a5", EncodeUtil.rfc5987Encode("日")); + + Assert.assertEquals("A" + "%20" + "%c3%a5" + "%e6%97%a5", + EncodeUtil.rfc5987Encode("A å日")); + } +} diff --git a/uitest/src/main/java/com/vaadin/tests/components/FileDownloaderUI.java b/uitest/src/main/java/com/vaadin/tests/components/FileDownloaderUI.java index 29c8ab5eaa..e4183cd40b 100644 --- a/uitest/src/main/java/com/vaadin/tests/components/FileDownloaderUI.java +++ b/uitest/src/main/java/com/vaadin/tests/components/FileDownloaderUI.java @@ -99,8 +99,8 @@ public class FileDownloaderUI extends AbstractTestUIWithLog { addComponents("Class resource pdf", resource, components); Button downloadUtf8File = new Button("Download UTF-8 named file"); - FileDownloader fd = new FileDownloader( - new ClassResource(new EmbeddedPdf().getClass(), "åäö-日本語.pdf")); + FileDownloader fd = new FileDownloader(new ClassResource( + new EmbeddedPdf().getClass(), "File åäö-日本語.pdf")); fd.setOverrideContentType(false); fd.extend(downloadUtf8File); addComponent(downloadUtf8File); diff --git "a/uitest/src/main/resources/com/vaadin/tests/components/embedded/\303\245\303\244\303\266-\346\227\245\346\234\254\350\252\236.pdf" "b/uitest/src/main/resources/com/vaadin/tests/components/embedded/File \303\245\303\244\303\266-\346\227\245\346\234\254\350\252\236.pdf" similarity index 100% rename from "uitest/src/main/resources/com/vaadin/tests/components/embedded/\303\245\303\244\303\266-\346\227\245\346\234\254\350\252\236.pdf" rename to "uitest/src/main/resources/com/vaadin/tests/components/embedded/File \303\245\303\244\303\266-\346\227\245\346\234\254\350\252\236.pdf" -- 2.39.5