From 62ba7dde9487809f6589abc63ddfd3748bd61bd9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 29 Mar 2019 14:12:48 -0600 Subject: [PATCH] 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. --- src/components/views/messages/MFileBody.js | 55 +++++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 7960db0384..372b501558 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -294,6 +294,8 @@ module.exports = React.createClass({ const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment"); const contentUrl = this._getContentUrl(); 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 (this.state.decryptedBlob === null) { @@ -372,6 +374,55 @@ module.exports = React.createClass({ ); } 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 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 // are being displayed in the room timeline or in a list of // files in the right hand side of the screen. @@ -379,7 +430,7 @@ module.exports = React.createClass({ return (
- + { fileName }
@@ -392,7 +443,7 @@ module.exports = React.createClass({ return (
- + { _t("Download %(text)s", { text: text }) }