diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index 92fd5833714..7c3ea772331 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -15,7 +15,6 @@ dependencies { optional("jakarta.el:jakarta.el-api") optional("jakarta.faces:jakarta.faces-api") optional("jakarta.json.bind:jakarta.json.bind-api") - optional("jakarta.mail:jakarta.mail-api") optional("jakarta.validation:jakarta.validation-api") optional("jakarta.xml.bind:jakarta.xml.bind-api") optional("io.reactivex.rxjava3:rxjava") diff --git a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java index 63149ae9e01..4793657852f 100644 --- a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java +++ b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -33,6 +33,7 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StreamUtils; import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.UTF_8; import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME; @@ -51,6 +52,9 @@ public final class ContentDisposition { private final static Pattern BASE64_ENCODED_PATTERN = Pattern.compile("=\\?([0-9a-zA-Z-_]+)\\?B\\?([+/0-9a-zA-Z]+=*)\\?="); + private final static Pattern QUOTED_PRINTABLE_ENCODED_PATTERN = + Pattern.compile("=\\?([0-9a-zA-Z-_]+)\\?Q\\?(\\p{Print}+)\\?="); + private static final String INVALID_HEADER_FIELD_PARAMETER_FORMAT = "Invalid header field parameter format (as defined in RFC 5987)"; @@ -371,12 +375,20 @@ public final class ContentDisposition { if (value.startsWith("=?") ) { Matcher matcher = BASE64_ENCODED_PATTERN.matcher(value); if (matcher.find()) { - String match1 = matcher.group(1); - String match2 = matcher.group(2); - filename = new String(Base64.getDecoder().decode(match2), Charset.forName(match1)); + charset = Charset.forName(matcher.group(1)); + String encodedValue = matcher.group(2); + filename = new String(Base64.getDecoder().decode(encodedValue), charset); } else { - filename = value; + matcher = QUOTED_PRINTABLE_ENCODED_PATTERN.matcher(value); + if (matcher.find()) { + charset = Charset.forName(matcher.group(1)); + String encodedValue = matcher.group(2); + filename = decodeQuotedPrintableFilename(encodedValue, charset); + } + else { + filename = value; + } } } else { @@ -498,6 +510,43 @@ public final class ContentDisposition { c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~'; } + /** + * Decode the given header field param as described in RFC 2047. + * @param filename the filename + * @param charset the charset for the filename + * @return the encoded header field param + * @see RFC 2047 + */ + private static String decodeQuotedPrintableFilename(String filename, Charset charset) { + Assert.notNull(filename, "'input' String` should not be null"); + Assert.notNull(charset, "'charset' should not be null"); + + byte[] value = filename.getBytes(US_ASCII); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int index = 0; + while (index < value.length) { + byte b = value[index]; + if (b == '_') { + baos.write(' '); + index++; + } + else if (b == '=' && index < value.length - 2) { + int i1 = Character.digit((char) value[index + 1], 16); + int i2 = Character.digit((char) value[index + 2], 16); + if (i1 == -1 || i2 == -1) { + throw new IllegalArgumentException("Not a valid hex sequence: " + filename.substring(index)); + } + baos.write((i1 << 4) | i2); + index += 3; + } + else { + baos.write(b); + index++; + } + } + return StreamUtils.copyToString(baos, charset); + } + private static String escapeQuotationsInFilename(String filename) { if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) { return filename; diff --git a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java index 73254647415..ed430a56232 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -18,7 +18,6 @@ package org.springframework.http.converter; import java.io.IOException; import java.io.OutputStream; -import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.Charset; @@ -29,9 +28,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import jakarta.mail.internet.MimeUtility; - import org.springframework.core.io.Resource; +import org.springframework.http.ContentDisposition; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; @@ -526,7 +524,13 @@ public class FormHttpMessageConverter implements HttpMessageConverter