Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.text.Normalizer;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
Expand Down Expand Up @@ -138,17 +139,48 @@ public void downloadFileZip(@PathVariable UUID uuid, @RequestParam("handleId") S
}

/**
* Build a Content-Disposition header value using RFC 5987 encoding.
* Includes both {@code filename} (ASCII fallback with escaped quotes) and {@code filename*}
* (UTF-8 percent-encoded) so that browsers can save files with special characters correctly.
* Build the Content-Disposition value the way vanilla's HttpHeadersInitializer does: an ASCII
* fallback in {@code filename} for clients that predate RFC 5987, plus the real UTF-8 name in
* {@code filename*} for everyone else. This endpoint has no upstream counterpart, so the logic
* is copied from vanilla rather than shared, to keep it tracking upstream's behaviour.
*/
private String buildContentDisposition(String name) {
String encoded = URLEncoder.encode(name, StandardCharsets.UTF_8)
.replace("+", "%20");
String asciiFallback = name.replaceAll("[^\\x20-\\x7E]", "_")
return String.format("attachment; filename=\"%s\"; filename*=UTF-8''%s",
createFallbackAsciiName(name), createEncodedUtf8Name(name));
}

/**
* Creates a safe ASCII-only fallback filename by removing diacritics (accents)
* and replacing any remaining non-ASCII characters.
* E.g., "ä-ö-é.pdf" becomes "a-o-e.pdf".
* @param originalFilename The original filename.
* @return A string containing only ASCII characters.
*/
private String createFallbackAsciiName(String originalFilename) {
if (originalFilename == null) {
return "";
}
String normalized = Normalizer.normalize(originalFilename, Normalizer.Form.NFD);
String withoutAccents = normalized.replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
// Deviates from vanilla: restrict to printable ASCII and escape \ and ". The value is a
// quoted-string, so control chars could inject a header and a quote would close it early.
// That is the bug #1267 fixed; vanilla still has it.
return withoutAccents.replaceAll("[^\\x20-\\x7E]", "")
.replace("\\", "\\\\")
.replace("\"", "\\\"");
return String.format("attachment; filename=\"%s\"; filename*=UTF-8''%s",
asciiFallback, encoded);
}

/**
* Creates a percent-encoded UTF-8 filename according to RFC 5987.
* This is for the `filename*` parameter.
* E.g., "ä ö é.pdf" becomes "%C3%A4%20%C3%B6%20%C3%A9.pdf".
* @param originalFilename The original filename.
* @return A percent-encoded string.
*/
private String createEncodedUtf8Name(String originalFilename) {
if (originalFilename == null) {
return "";
}
return URLEncoder.encode(originalFilename, StandardCharsets.UTF_8).replace("+", "%20");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@

import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static javax.mail.internet.MimeUtility.encodeText;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.Normalizer;
import java.util.Arrays;
import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -165,9 +167,16 @@ public HttpHeaders initialiseHeaders() throws IOException {

httpHeaders.put(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS,
Collections.singletonList(HttpHeaders.ACCEPT_RANGES));
httpHeaders.put(CONTENT_DISPOSITION, Collections.singletonList(String.format(CONTENT_DISPOSITION_FORMAT,
disposition,
encodeText(fileName))));
String fallbackAsciiName = createFallbackAsciiName(this.fileName);
String encodedUtf8Name = createEncodedUtf8Name(this.fileName);

String headerValue = String.format(
"%s; filename=\"%s\"; filename*=UTF-8''%s",
disposition,
fallbackAsciiName,
encodedUtf8Name
);
Comment thread
milanmajchrak marked this conversation as resolved.
httpHeaders.put(CONTENT_DISPOSITION, Collections.singletonList(headerValue));
log.debug("Content-Disposition : {}", disposition);

// Content phase
Expand Down Expand Up @@ -260,4 +269,46 @@ private static boolean matches(String matchHeader, String toMatch) {
return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1;
}

/**
* Creates a safe ASCII-only fallback filename by removing diacritics (accents)
* and replacing any remaining non-ASCII characters.
* E.g., "ä-ö-é.pdf" becomes "a-o-e.pdf".
* @param originalFilename The original filename.
* @return A string containing only ASCII characters.
*/
private String createFallbackAsciiName(String originalFilename) {
if (originalFilename == null) {
return "";
}
String normalized = Normalizer.normalize(originalFilename, Normalizer.Form.NFD);
String withoutAccents = normalized.replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
// Deviates from vanilla: restrict to printable ASCII and escape \ and ". The value is a
// quoted-string, so control chars could inject a header and a quote would close it early.
// Kept consistent with MetadataBitstreamController.createFallbackAsciiName.
return withoutAccents.replaceAll("[^\\x20-\\x7E]", "")
.replace("\\", "\\\\")
.replace("\"", "\\\"");
}

/**
* Creates a percent-encoded UTF-8 filename according to RFC 5987.
* This is for the `filename*` parameter.
* E.g., "ä ö é.pdf" becomes "%C3%A4%20%C3%B6%20%C3%A9.pdf".
* @param originalFilename The original filename.
* @return A percent-encoded string.
*/
private String createEncodedUtf8Name(String originalFilename) {
if (originalFilename == null) {
return "";
}
try {
String encoded = URLEncoder.encode(originalFilename, StandardCharsets.UTF_8.toString());
return encoded.replace("+", "%20");
} catch (java.io.UnsupportedEncodingException e) {
// Fallback to a simple ASCII name if encoding fails.
log.error("UTF-8 encoding not supported, which should not happen.", e);
return createFallbackAsciiName(originalFilename);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
package org.dspace.app.rest;

import static java.util.UUID.randomUUID;
import static javax.mail.internet.MimeUtility.encodeText;
import static org.apache.commons.codec.CharEncoding.UTF_8;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.apache.commons.io.IOUtils.toInputStream;
Expand Down Expand Up @@ -332,7 +331,11 @@ public void testBitstreamName() throws Exception {
//2. A public item with a bitstream

String bitstreamContent = "0123456789";
String bitstreamName = "ภาษาไทย";
String bitstreamName = "ภาษาไทย-com-acentuação.pdf";
String expectedAscii = "-com-acentuacao.pdf";
String expectedUtf8Encoded =
"%E0%B8%A0%E0%B8%B2%E0%B8%A9%E0%B8%B2%E0%B9%84%E0%B8%97%E0%B8%A2-"
+ "com-acentua%C3%A7%C3%A3o.pdf";

try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {

Expand All @@ -356,7 +359,9 @@ public void testBitstreamName() throws Exception {
//We expect the content disposition to have the encoded bitstream name
.andExpect(header().string(
"Content-Disposition",
"attachment;filename=\"" + encodeText(bitstreamName) + "\""
String.format("attachment; filename=\"%s\"; filename*=UTF-8''%s",
expectedAscii,
expectedUtf8Encoded)
));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,9 @@ public void downloadAllZipWithNonAsciiItemName() throws Exception {
getClient(token).perform(get(METADATABITSTREAM_ENDPOINT + "/" + itemWithDiacritics.getID() +
"/" + ALL_ZIP_PATH).param(HANDLE_PARAM, itemWithDiacritics.getHandle()))
.andExpect(status().isOk())
// Non-ASCII chars replaced with _ in filename, full UTF-8 in filename*
// fallback transliterates the diacritics away; filename* carries the real name
.andExpect(header().string("Content-Disposition",
"attachment; filename=\"P__li_ _lu_ou_k_ k__.zip\";"
"attachment; filename=\"Prilis zlutoucky kun.zip\";"
+ " filename*=UTF-8''P%C5%99%C3%ADli%C5%A1%20%C5%BElu%C5%A5ou%C4%8Dk%C3%BD"
+ "%20k%C5%AF%C5%88.zip"));
}
Expand Down
Loading