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 @@ -6,51 +6,76 @@

public enum DownloadRouteMimeType {

jpg(new String[]{"jpg", "jpeg"}, "image/jpeg"),
png(new String[]{"png"}, "image/png"),
gif(new String[]{"gif"}, "image/gif"),
pdf(new String[]{"pdf"}, "application/pdf"),
html(new String[]{"htm", "html"}, "text/html"),
mp4(new String[]{"mp4", "mov"}, "video/mp4"),
mpeg(new String[]{"mpeg", "mpg"}, "video/mpeg"),
webm(new String[]{"webm"}, "video/webm"),
ogv(new String[]{"ogv"}, "video/ogg"),
wildcard(new String[]{""}, "*/*");
// Formats browsers can safely render in a tab.
jpg(new String[]{"jpg", "jpeg"}, "image/jpeg", true),
png(new String[]{"png"}, "image/png", true),
gif(new String[]{"gif"}, "image/gif", true),
pdf(new String[]{"pdf"}, "application/pdf", true),
mp4(new String[]{"mp4", "mov"}, "video/mp4", true),
mpeg(new String[]{"mpeg", "mpg"}, "video/mpeg", true),
webm(new String[]{"webm"}, "video/webm", true),
ogv(new String[]{"ogv"}, "video/ogg", true),

// Rendering uploaded html inline would run its script in the app origin.
html(new String[]{"htm", "html"}, "text/html", false),

// Correct type so the OS picks the right app, but always delivered as a download.
docx(new String[]{"docx"}, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", false),
xlsx(new String[]{"xlsx"}, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", false),
pptx(new String[]{"pptx"}, "application/vnd.openxmlformats-officedocument.presentationml.presentation", false),
doc(new String[]{"doc"}, "application/msword", false),
xls(new String[]{"xls"}, "application/vnd.ms-excel", false),
ppt(new String[]{"ppt"}, "application/vnd.ms-powerpoint", false),
odt(new String[]{"odt"}, "application/vnd.oasis.opendocument.text", false),
ods(new String[]{"ods"}, "application/vnd.oasis.opendocument.spreadsheet", false),
odp(new String[]{"odp"}, "application/vnd.oasis.opendocument.presentation", false),
zip(new String[]{"zip"}, "application/zip", false),
csv(new String[]{"csv"}, "text/csv", false),
txt(new String[]{"txt"}, "text/plain", false),
json(new String[]{"json"}, "application/json", false),
xml(new String[]{"xml"}, "application/xml", false),

// Unrecognized extensions force a download rather than letting the browser guess.
unknown(new String[]{""}, "application/octet-stream", false);

private String[] extension;
private String mimeType;
private boolean inlineViewable;

private DownloadRouteMimeType(String[] ext, String value) {
private DownloadRouteMimeType(String[] ext, String value, boolean inlineViewable) {
this.extension = ext;
this.mimeType = value;
this.inlineViewable = inlineViewable;
}

public static String getTypeForFilename(String fileName) {
String type = DownloadRouteMimeType.wildcard.mimeType;
private static DownloadRouteMimeType getForFilename(String fileName) {
if (fileName == null) {
return DownloadRouteMimeType.unknown;
}

String[] split = fileName.split("[.]");
String ext = null;
if (split.length > 0) {
ext = split[split.length - 1].toLowerCase();
} else {
return type;
if (split.length < 2) {
return DownloadRouteMimeType.unknown;
}

DownloadRouteMimeType[] values = DownloadRouteMimeType.values();
boolean done = false;
for (DownloadRouteMimeType value : values) {
String ext = split[split.length - 1].toLowerCase();

for (DownloadRouteMimeType value : DownloadRouteMimeType.values()) {
for (String possibleExt : value.extension) {
if (possibleExt.equals(ext)) {
type = value.mimeType;
done = true;
break;
return value;
}
}
if (done) {
break;
}
}

return type;
return DownloadRouteMimeType.unknown;
}

public static String getTypeForFilename(String fileName) {
return getForFilename(fileName).mimeType;
}

/** Whether the browser may display the file inline rather than downloading it. */
public static boolean isInlineViewable(String fileName) {
return getForFilename(fileName).inlineViewable;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.ejb.EJB;
Expand Down Expand Up @@ -197,13 +200,11 @@ private Response getFileResponse(String errorFileTypeColonName, String fileName,
File file = new File(storageFilePath);

if (file.exists()) {
String headerObject = "";
if (isAttachment) {
headerObject += "attachment; ";
} else {
headerObject += "inline; ";
}
headerObject += "filename=" + fileName;
// Callers may force a download; otherwise the resolved type decides.
boolean forceDownload = isAttachment || !DownloadRouteMimeType.isInlineViewable(fileName);

String headerObject = forceDownload ? "attachment; " : "inline; ";
headerObject += buildFilenameHeaderParameters(fileName);

ResponseBuilder response = null;

Expand All @@ -214,6 +215,9 @@ private Response getFileResponse(String errorFileTypeColonName, String fileName,
headerObject);

response.header("Content-Length", file.length());

// Keep browsers from sniffing a different type than the one we declared.
response.header("X-Content-Type-Options", "nosniff");

return response.build();
}
Expand All @@ -223,4 +227,41 @@ private Response getFileResponse(String errorFileTypeColonName, String fileName,
throw fileNotFoundException;
}

/** RFC 6266 filename parameters: quoted ASCII fallback plus RFC 5987 encoded form. */
private String buildFilenameHeaderParameters(String fileName) {
if (fileName == null || fileName.isEmpty()) {
fileName = "download";
}

// Never let a path escape into the suggested name.
fileName = fileName.replace('\\', '/');
int lastSeparator = fileName.lastIndexOf('/');
if (lastSeparator >= 0) {
fileName = fileName.substring(lastSeparator + 1);
}
if (fileName.isEmpty()) {
fileName = "download";
}

StringBuilder asciiFallback = new StringBuilder();
for (char c : fileName.toCharArray()) {
if (c < 32 || c > 126 || c == '"' || c == '\\') {
asciiFallback.append('_');
} else {
asciiFallback.append(c);
}
}

String encoded;
try {
encoded = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())
.replace("+", "%20");
} catch (UnsupportedEncodingException ex) {
// UTF-8 is always available.
encoded = asciiFallback.toString();
}

return "filename=\"" + asciiFallback + "\"; filename*=UTF-8''" + encoded;
}

}
Loading