Download PDFs as blobs to avoid empty grey screens

Fixes https://github.com/vector-im/riot-web/issues/8605

The grey screen of sadness comes up when Chrome tries to open the PDF but doesn't have the right CSP headers. To avoid this, we'll just force a download of the PDF through `fetch` and `Blob`. 

There are a few cases where the user might still get a grey screen though: namely if they open the URL in a new tab or when the event content is lying about the file type, or the file is too large to blobify.

`fetch` works in Chrome, Firefox, and our packaged Electron version.
This commit is contained in:
Travis Ralston 2019-03-29 14:12:48 -06:00
parent 05e47766b4
commit 62ba7dde94

View file

@ -294,6 +294,8 @@ module.exports = React.createClass({
const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment"); const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment");
const contentUrl = this._getContentUrl(); const contentUrl = this._getContentUrl();
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const fileSize = content.info ? content.info.size : null;
const fileType = content.info ? content.info.mimetype : "application/octet-stream";
if (isEncrypted) { if (isEncrypted) {
if (this.state.decryptedBlob === null) { if (this.state.decryptedBlob === null) {
@ -372,6 +374,55 @@ module.exports = React.createClass({
</span> </span>
); );
} else if (contentUrl) { } else if (contentUrl) {
const downloadProps = {
target: "_blank",
rel: "noopener",
// We set the href regardless of whether or not we intercept the download
// because we don't really want to convert the file to a blob eagerly, and
// still want "open in new tab" and "save link as" to work.
href: contentUrl,
};
// Blobs can only have up to 500mb, so if the file reports as being too large then
// we won't try and convert it. Likewise, if the file size is unknown then we'll assume
// it is too big. There is the risk of the reported file size and the actual file size
// being different, however the user shouldn't normally run into this problem.
const fileTooBig = typeof(fileSize) === 'number' ? fileSize > 524288000 : true;
if (["application/pdf"].includes(fileType) && !fileTooBig) {
// We want to force a download on this type, so use an onClick handler.
downloadProps["onClick"] = (e) => {
console.log(`Downloading ${fileType} as blob (unencrypted)`);
// Avoid letting the <a> do its thing
e.preventDefault();
e.stopPropagation();
// Start a fetch for the download
// Based upon https://stackoverflow.com/a/49500465
fetch(contentUrl, {
headers: new Headers({
'Origin': window.location.origin,
}),
mode: 'cors',
}).then((response) => response.blob()).then((blob) => {
const blobUrl = URL.createObjectURL(blob);
// We have to create an anchor to download the file
const tempAnchor = document.createElement('a');
tempAnchor.download = fileName;
tempAnchor.href = blobUrl;
document.body.appendChild(tempAnchor); // for firefox: https://stackoverflow.com/a/32226068
tempAnchor.click();
tempAnchor.remove();
});
};
} else {
// Else we are hoping the browser will do the right thing
downloadProps["download"] = fileName;
}
// If the attachment is not encrypted then we check whether we // If the attachment is not encrypted then we check whether we
// are being displayed in the room timeline or in a list of // are being displayed in the room timeline or in a list of
// files in the right hand side of the screen. // files in the right hand side of the screen.
@ -379,7 +430,7 @@ module.exports = React.createClass({
return ( return (
<span className="mx_MFileBody"> <span className="mx_MFileBody">
<div className="mx_MFileBody_download"> <div className="mx_MFileBody_download">
<a className="mx_MFileBody_downloadLink" href={contentUrl} download={fileName} target="_blank"> <a className="mx_MFileBody_downloadLink" {...downloadProps}>
{ fileName } { fileName }
</a> </a>
<div className="mx_MImageBody_size"> <div className="mx_MImageBody_size">
@ -392,7 +443,7 @@ module.exports = React.createClass({
return ( return (
<span className="mx_MFileBody"> <span className="mx_MFileBody">
<div className="mx_MFileBody_download"> <div className="mx_MFileBody_download">
<a href={contentUrl} download={fileName} target="_blank" rel="noopener"> <a {...downloadProps}>
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage" /> <img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage" />
{ _t("Download %(text)s", { text: text }) } { _t("Download %(text)s", { text: text }) }
</a> </a>