diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index 8fd7b8b178..72e81e4a79 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -16,6 +16,8 @@ package org.springframework.util; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -50,6 +52,7 @@ import java.util.TimeZone; * @author Rick Evans * @author Arjen Poutsma * @author Sam Brannen + * @author Brian Clozel * @since 16 April 2001 */ public abstract class StringUtils { @@ -1193,4 +1196,44 @@ public abstract class StringUtils { return arrayToDelimitedString(arr, ","); } + /** + * Encode the given header field param as describe in the rfc5987. + * @param input the header field param + * @param charset the charset of the header field param string + * @return the encoded header field param + * @see rfc5987 + * @since 5.0 + */ + public static String encodeHttpHeaderFieldParam(String input, Charset charset) { + Assert.notNull(charset, "charset should not be null"); + if(StandardCharsets.US_ASCII.equals(charset)) { + return input; + } + Assert.isTrue(StandardCharsets.UTF_8.equals(charset) || StandardCharsets.ISO_8859_1.equals(charset), + "charset should be UTF-8 or ISO-8859-1"); + final byte[] source = input.getBytes(charset); + final int len = source.length; + final StringBuilder sb = new StringBuilder(len << 1); + sb.append(charset.name()); + sb.append("''"); + for (byte b : source) { + if (isRFC5987AttrChar(b)) { + sb.append((char) b); + } + else { + sb.append('%'); + char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16)); + char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16)); + sb.append(hex1); + sb.append(hex2); + } + } + return sb.toString(); + } + + private static boolean isRFC5987AttrChar(byte c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') + || c == '!' || c == '#' || c == '$' || c == '&' || c == '+' || c == '-' + || c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~'; + } } diff --git a/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java b/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java index 22e979e455..2b574aaca5 100644 --- a/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.util; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Locale; import java.util.Properties; @@ -700,4 +701,19 @@ public class StringUtilsTests { assertEquals("Variant containing country code not extracted correctly", variant, locale.getVariant()); } + // SPR-14547 + @Test + public void encodeHttpHeaderFieldParam() { + String result = StringUtils.encodeHttpHeaderFieldParam("test.txt", StandardCharsets.US_ASCII); + assertEquals("test.txt", result); + + result = StringUtils.encodeHttpHeaderFieldParam("中文.txt", StandardCharsets.UTF_8); + assertEquals("UTF-8''%E4%B8%AD%E6%96%87.txt", result); + } + + @Test(expected = IllegalArgumentException.class) + public void encodeHttpHeaderFieldParamInvalidCharset() { + StringUtils.encodeHttpHeaderFieldParam("test", StandardCharsets.UTF_16); + } + } diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index 664f658348..994437acde 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -19,6 +19,7 @@ package org.springframework.http; import java.io.Serializable; import java.net.URI; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -672,12 +673,32 @@ public class HttpHeaders implements MultiValueMap, Serializable * @param filename the filename (may be {@code null}) */ public void setContentDispositionFormData(String name, String filename) { + setContentDispositionFormData(name, filename, null); + } + + /** + * Set the (new) value of the {@code Content-Disposition} header + * for {@code form-data}, optionally encoding the filename using the rfc5987. + *

Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported. + * @param name the control name + * @param filename the filename (may be {@code null}) + * @param charset the charset used for the filename (may be {@code null}) + * @see rfc7230 Section 3.2.4 + * @since 5.0 + */ + public void setContentDispositionFormData(String name, String filename, Charset charset) { Assert.notNull(name, "'name' must not be null"); StringBuilder builder = new StringBuilder("form-data; name=\""); builder.append(name).append('\"'); if (filename != null) { - builder.append("; filename=\""); - builder.append(filename).append('\"'); + if(charset == null || StandardCharsets.US_ASCII.equals(charset)) { + builder.append("; filename=\""); + builder.append(filename).append('\"'); + } + else { + builder.append("; filename*="); + builder.append(StringUtils.encodeHttpHeaderFieldParam(filename, charset)); + } } set(CONTENT_DISPOSITION, builder.toString()); } diff --git a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java index 0e7a89ceac..a22f89c70c 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java @@ -321,6 +321,11 @@ public class HttpHeadersTests { headers.setContentDispositionFormData("name", "filename"); assertEquals("Invalid Content-Disposition header", "form-data; name=\"name\"; filename=\"filename\"", headers.getFirst("Content-Disposition")); + + headers.setContentDispositionFormData("name", "中文.txt", StandardCharsets.UTF_8); + assertEquals("Invalid Content-Disposition header", + "form-data; name=\"name\"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt", + headers.getFirst("Content-Disposition")); } @Test // SPR-11917