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 @@ -11,6 +11,8 @@

import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -110,7 +112,7 @@ public void downloadFileZip(@PathVariable UUID uuid, @RequestParam("handleId") S

Item item = (Item) dso;
name = item.getName() + ".zip";
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment;filename=\"%s\"", name));
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, buildContentDisposition(name));
response.setContentType("application/zip");
List<Bundle> bundles = item.getBundles("ORIGINAL");

Expand All @@ -134,4 +136,19 @@ public void downloadFileZip(@PathVariable UUID uuid, @RequestParam("handleId") S
zip.close();
response.getOutputStream().flush();
}

/**
* 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.
*/
private String buildContentDisposition(String name) {
String encoded = URLEncoder.encode(name, StandardCharsets.UTF_8)
.replace("+", "%20");
String asciiFallback = name.replaceAll("[^\\x20-\\x7E]", "_")
.replace("\\", "\\\\")
.replace("\"", "\\\"");
return String.format("attachment; filename=\"%s\"; filename*=UTF-8''%s",
asciiFallback, encoded);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.io.ByteArrayOutputStream;
Expand Down Expand Up @@ -97,4 +98,66 @@ public void downloadAllZip() throws Exception {
.andExpect(content().bytes(byteArrayOutputStream.toByteArray()));

}

@Test
public void downloadAllZipWithDoubleQuotesInItemName() throws Exception {
context.turnOffAuthorisationSystem();

// Create an item with double quotes in the name — reproduces the
// ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_DISPOSITION browser error.
Item itemWithQuotes = ItemBuilder.createItem(context, col)
.withTitle("Supported data for manuscript \"Thermally-induced evolution\"")
.withAuthor(AUTHOR)
.build();

String bitstreamContent = "QuotedItemContent";
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
BitstreamBuilder
.createBitstream(context, itemWithQuotes, is)
.withName("data.csv")
.withMimeType("text/csv")
.build();
}
context.restoreAuthSystemState();

String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(get(METADATABITSTREAM_ENDPOINT + "/" + itemWithQuotes.getID() +
"/" + ALL_ZIP_PATH).param(HANDLE_PARAM, itemWithQuotes.getHandle()))
.andExpect(status().isOk())
// The filename must have escaped quotes so the header is valid
.andExpect(header().string("Content-Disposition",
"attachment; filename=\"Supported data for manuscript"
+ " \\\"Thermally-induced evolution\\\".zip\";"
+ " filename*=UTF-8''Supported%20data%20for%20manuscript"
+ "%20%22Thermally-induced%20evolution%22.zip"));
}

@Test
public void downloadAllZipWithNonAsciiItemName() throws Exception {
context.turnOffAuthorisationSystem();

Item itemWithDiacritics = ItemBuilder.createItem(context, col)
.withTitle("Příliš žluťoučký kůň")
.withAuthor(AUTHOR)
.build();

String bitstreamContent = "DiacriticsContent";
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
BitstreamBuilder.createBitstream(context, itemWithDiacritics, is)
.withName("file.txt")
.withMimeType("text/plain")
.build();
}
context.restoreAuthSystemState();

String token = getAuthToken(admin.getEmail(), password);
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*
.andExpect(header().string("Content-Disposition",
"attachment; filename=\"P__li_ _lu_ou_k_ k__.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"));
}
}
Loading