Browse Source

Use proper UTF-8 encoding for Content-Disposition filenames (#19527) (#6607)

tags/7.7.7
Leif Åstrand 7 years ago
parent
commit
94df5a7ed3

+ 6
- 9
server/src/main/java/com/vaadin/server/DownloadStream.java View File

@@ -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.
* <p>
@@ -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);
}

/**

+ 71
- 0
server/src/main/java/com/vaadin/util/EncodeUtil.java View File

@@ -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 <code>value-chars</code> as defined in
* RFC5987 for use in e.g. the <code>Content-Disposition</code> HTTP header.
*
* @param value
* the string to encode, not <code>null</code>
* @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);
}
}

}

+ 10
- 6
server/src/test/java/com/vaadin/server/DownloadStreamTest.java View File

@@ -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)));
}
}

+ 7
- 6
server/src/test/java/com/vaadin/tests/server/ClassesSerializableTest.java View File

@@ -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);
}

+ 32
- 0
server/src/test/java/com/vaadin/util/EncodeUtilTest.java View File

@@ -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 å日"));
}
}

+ 2
- 2
uitest/src/main/java/com/vaadin/tests/components/FileDownloaderUI.java View File

@@ -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);

uitest/src/main/resources/com/vaadin/tests/components/embedded/åäö-日本語.pdf → uitest/src/main/resources/com/vaadin/tests/components/embedded/File åäö-日本語.pdf View File


Loading…
Cancel
Save