Support multipart filename with charset

StandardMultipartHttpServletRequest now supports filenames with
charset information.

Issue: SPR-13319
This commit is contained in:
Rossen Stoyanchev 2015-08-25 20:52:30 -04:00
parent 4314da922a
commit 1a9e42b49d
2 changed files with 94 additions and 12 deletions

View File

@ -20,6 +20,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -44,6 +45,7 @@ import org.springframework.web.multipart.MultipartFile;
* methods - without any custom processing on our side.
*
* @author Juergen Hoeller
* @author Rossen Stoyanchev
* @since 3.1
*/
public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {
@ -52,6 +54,11 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
private static final String FILENAME_KEY = "filename=";
private static final String FILENAME_WITH_CHARSET_KEY = "filename*=";
private static final Charset US_ASCII = Charset.forName("us-ascii");
private Set<String> multipartParameterNames;
@ -86,7 +93,11 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
this.multipartParameterNames = new LinkedHashSet<String>(parts.size());
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<String, MultipartFile>(parts.size());
for (Part part : parts) {
String filename = extractFilename(part.getHeader(CONTENT_DISPOSITION));
String disposition = part.getHeader(CONTENT_DISPOSITION);
String filename = extractFilename(disposition);
if (filename == null) {
filename = extractFilenameWithCharset(disposition);
}
if (filename != null) {
files.add(part.getName(), new StandardMultipartFile(part, filename));
}
@ -102,15 +113,18 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
}
private String extractFilename(String contentDisposition) {
return extractFilename(contentDisposition, FILENAME_KEY);
}
private String extractFilename(String contentDisposition, String key) {
if (contentDisposition == null) {
return null;
}
// TODO: can only handle the typical case at the moment
int startIndex = contentDisposition.indexOf(FILENAME_KEY);
int startIndex = contentDisposition.indexOf(key);
if (startIndex == -1) {
return null;
}
String filename = contentDisposition.substring(startIndex + FILENAME_KEY.length());
String filename = contentDisposition.substring(startIndex + key.length());
if (filename.startsWith("\"")) {
int endIndex = filename.indexOf("\"", 1);
if (endIndex != -1) {
@ -126,6 +140,33 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
return filename;
}
private String extractFilenameWithCharset(String contentDisposition) {
String filename = extractFilename(contentDisposition, FILENAME_WITH_CHARSET_KEY);
if (filename == null) {
return null;
}
int index = filename.indexOf("'");
if (index != -1) {
Charset charset = null;
try {
charset = Charset.forName(filename.substring(0, index));
}
catch (IllegalArgumentException ex) {
// ignore
}
filename = filename.substring(index + 1);
// Skip language information..
index = filename.indexOf("'");
if (index != -1) {
filename = filename.substring(index + 1);
}
if (charset != null) {
filename = new String(filename.getBytes(US_ASCII), charset);
}
}
return filename;
}
@Override
protected void initializeMultipart() {

View File

@ -19,9 +19,9 @@ package org.springframework.web.servlet.mvc.method.annotation;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.servlet.MultipartConfigElement;
import org.eclipse.jetty.server.Connector;
@ -29,7 +29,6 @@ import org.eclipse.jetty.server.NetworkConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
@ -43,6 +42,7 @@ import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
@ -52,9 +52,9 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.stereotype.Controller;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
@ -66,7 +66,8 @@ import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
/**
* Test access to parts of a multipart request with {@link RequestPart}.
@ -117,7 +118,7 @@ public class RequestPartIntegrationTests {
@Before
public void setUp() {
ByteArrayHttpMessageConverter emptyBodyConverter = new ByteArrayHttpMessageConverter();
emptyBodyConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON));
emptyBodyConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON));
List<HttpMessageConverter<?>> converters = new ArrayList<>(3);
converters.add(emptyBodyConverter);
@ -129,7 +130,7 @@ public class RequestPartIntegrationTests {
converter.setPartConverters(converters);
restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
restTemplate.setMessageConverters(Arrays.<HttpMessageConverter<?>>asList(converter));
restTemplate.setMessageConverters(Collections.singletonList(converter));
}
@AfterClass
@ -150,6 +151,37 @@ public class RequestPartIntegrationTests {
testCreate(baseUrl + "/standard-resolver/test");
}
// SPR-13319
@Test
public void standardMultipartResolverWithEncodedFileName() throws Exception {
byte[] boundary = MimeTypeUtils.generateMultipartBoundary();
String boundaryText = new String(boundary, "US-ASCII");
Map<String, String> params = Collections.singletonMap("boundary", boundaryText);
String content =
"--" + boundaryText + "\n" +
"Content-Disposition: form-data; name=\"file\"; filename*=\"utf-8''%C3%A9l%C3%A8ve.txt\"\n" +
"Content-Type: text/plain\n" +
"Content-Length: 7\n" +
"\n" +
"content\n" +
"--" + boundaryText + "--";
RequestEntity<byte[]> requestEntity =
RequestEntity.post(new URI(baseUrl + "/standard-resolver/spr13319"))
.contentType(new MediaType(MediaType.MULTIPART_FORM_DATA, params))
.body(content.getBytes(Charset.forName("us-ascii")));
ByteArrayHttpMessageConverter converter = new ByteArrayHttpMessageConverter();
converter.setSupportedMediaTypes(Collections.singletonList(MediaType.MULTIPART_FORM_DATA));
this.restTemplate.setMessageConverters(Collections.singletonList(converter));
ResponseEntity<Void> responseEntity = restTemplate.exchange(requestEntity, Void.class);
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
}
private void testCreate(String url) {
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>();
parts.add("json-data", new HttpEntity<TestData>(new TestData("Jason")));
@ -176,6 +208,7 @@ public class RequestPartIntegrationTests {
}
@Configuration
@SuppressWarnings("unused")
static class CommonsMultipartResolverTestConfig extends RequestPartTestConfig {
@Bean
@ -185,6 +218,7 @@ public class RequestPartIntegrationTests {
}
@Configuration
@SuppressWarnings("unused")
static class StandardMultipartResolverTestConfig extends RequestPartTestConfig {
@Bean
@ -194,9 +228,10 @@ public class RequestPartIntegrationTests {
}
@Controller
@SuppressWarnings("unused")
private static class RequestPartTestController {
@RequestMapping(value = "/test", method = RequestMethod.POST, consumes = { "multipart/mixed", "multipart/form-data" })
@RequestMapping(value = "/test", method = POST, consumes = { "multipart/mixed", "multipart/form-data" })
public ResponseEntity<Object> create(@RequestPart(name = "json-data") TestData testData,
@RequestPart("file-data") MultipartFile file,
@RequestPart(name = "empty-data", required = false) TestData emptyData,
@ -209,6 +244,12 @@ public class RequestPartIntegrationTests {
headers.setLocation(URI.create(url));
return new ResponseEntity<Object>(headers, HttpStatus.CREATED);
}
@RequestMapping(value = "/spr13319", method = POST, consumes = "multipart/form-data")
public ResponseEntity<Void> create(@RequestPart("file") MultipartFile multipartFile) {
assertEquals("%C3%A9l%C3%A8ve.txt", multipartFile.getOriginalFilename());
return ResponseEntity.ok().build();
}
}
@SuppressWarnings("unused")