Revert "remove code related to encrypted file download button in iframe" (#7957)

* Revert "remove code related to encrypted file download button in iframe (#7940)"

This reverts commit 26216ec527.

* udpate icon import in MFileBody

Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
Kerry 2022-03-02 18:27:36 +01:00 committed by GitHub
parent 8f68a43ee3
commit d304e24a45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 88 additions and 5 deletions

View file

@ -45,6 +45,20 @@ limitations under the License.
pointer-events: none;
}
/* Remove the border and padding for iframes for download links. */
.mx_MFileBody_download iframe {
margin: 0px;
padding: 0px;
border: none;
width: 100%;
/* Set the height of the iframe to be 1 line of text.
* Iframes don't automatically size themselves to fit their content.
* So either we have to fix the height of the iframe using CSS or
* use javascript's cross-origin postMessage API to communicate how
* big the content of the iframe is. */
height: 1.5em;
}
.mx_MFileBody_info {
cursor: pointer;

View file

@ -30,6 +30,20 @@ import { IBodyProps } from "./IBodyProps";
import { FileDownloader } from "../../../utils/FileDownloader";
import TextWithTooltip from "../elements/TextWithTooltip";
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import DownloadSvg from '../../../../res/img/download.svg';
export let DOWNLOAD_ICON_URL; // cached copy of the download.svg asset for the sandboxed iframe later on
async function cacheDownloadIcon() {
if (DOWNLOAD_ICON_URL) return; // cached already
// eslint-disable-next-line @typescript-eslint/no-var-requires
const svg = await fetch(DownloadSvg).then(r => r.text());
DOWNLOAD_ICON_URL = "data:image/svg+xml;base64," + window.btoa(svg);
}
// Cache the asset immediately
// noinspection JSIgnoredPromiseFromCall
cacheDownloadIcon();
// User supplied content can contain scripts, we have to be careful that
// we don't accidentally run those script within the same origin as the
@ -61,6 +75,29 @@ import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContex
// the downside of using a sandboxed iframe is that the browers are overly
// restrictive in what you are allowed to do with the generated URL.
/**
* Get the current CSS style for a DOMElement.
* @param {HTMLElement} element The element to get the current style of.
* @return {string} The CSS style encoded as a string.
*/
export function computedStyle(element: HTMLElement) {
if (!element) {
return "";
}
const style = window.getComputedStyle(element, null);
let cssText = style.cssText;
// noinspection EqualityComparisonWithCoercionJS
if (cssText == "") {
// Firefox doesn't implement ".cssText" for computed styles.
// https://bugzilla.mozilla.org/show_bug.cgi?id=137687
for (let i = 0; i < style.length; i++) {
cssText += style[i] + ":";
cssText += style.getPropertyValue(style[i]) + ";";
}
}
return cssText;
}
interface IProps extends IBodyProps {
/* whether or not to show the default placeholder for the file. Defaults to true. */
showGenericPlaceholder: boolean;
@ -79,9 +116,10 @@ export default class MFileBody extends React.Component<IProps, IState> {
showGenericPlaceholder: true,
};
private iframe: React.RefObject<HTMLIFrameElement> = createRef();
private dummyLink: React.RefObject<HTMLAnchorElement> = createRef();
private userDidClick = false;
private fileDownloader: FileDownloader = new FileDownloader();
private fileDownloader: FileDownloader = new FileDownloader(() => this.iframe.current);
public constructor(props: IProps) {
super(props);
@ -106,11 +144,17 @@ export default class MFileBody extends React.Component<IProps, IState> {
return presentableTextForFile(this.content);
}
private downloadFile(fileName: string) {
private downloadFile(fileName: string, text: string) {
this.fileDownloader.download({
blob: this.state.decryptedBlob,
name: fileName,
autoDownload: this.userDidClick,
opts: {
imgSrc: DOWNLOAD_ICON_URL,
imgStyle: null,
style: computedStyle(this.dummyLink.current),
textContent: _t("Download %(text)s", { text }),
},
});
}
@ -142,7 +186,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
const mediaHelper = this.props.mediaEventHelper;
if (mediaHelper?.media.isEncrypted) {
await this.decryptFile();
this.downloadFile(this.fileName);
this.downloadFile(this.fileName, this.linkText);
} else {
// As a button we're missing the `download` attribute for styling reasons, so
// download with the file downloader.
@ -213,6 +257,8 @@ export default class MFileBody extends React.Component<IProps, IState> {
);
}
const url = "usercontent/"; // XXX: this path should probably be passed from the skin
// If the attachment is encrypted then put the link inside an iframe.
return (
<span className="mx_MFileBody">
@ -229,6 +275,20 @@ export default class MFileBody extends React.Component<IProps, IState> {
{ /* eslint-disable-next-line */ }
<a ref={this.dummyLink} />
</div>
{ /*
TODO: Move iframe (and dummy link) into FileDownloader.
We currently have it set up this way because of styles applied to the iframe
itself which cannot be easily handled/overridden by the FileDownloader. In
future, the download link may disappear entirely at which point it could also
be suitable to just remove this bit of code.
*/ }
<iframe
aria-hidden
title={presentableTextForFile(this.content, _t("Attachment"), true, true)}
src={url}
onLoad={() => this.downloadFile(this.fileName, this.linkText)}
ref={this.iframe}
sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation" />
</div> }
</span>
);

View file

@ -2105,9 +2105,9 @@
"Reply": "Reply",
"Collapse quotes │ ⇧+click": "Collapse quotes │ ⇧+click",
"Expand quotes │ ⇧+click": "Expand quotes │ ⇧+click",
"Download %(text)s": "Download %(text)s",
"Error decrypting attachment": "Error decrypting attachment",
"Decrypt %(text)s": "Decrypt %(text)s",
"Download %(text)s": "Download %(text)s",
"Invalid file%(extra)s": "Invalid file%(extra)s",
"Error decrypting image": "Error decrypting image",
"Show image": "Show image",

View file

@ -16,10 +16,18 @@ limitations under the License.
export type getIframeFn = () => HTMLIFrameElement; // eslint-disable-line @typescript-eslint/naming-convention
export const DEFAULT_STYLES = {
imgSrc: "",
imgStyle: null, // css props
style: "",
textContent: "",
};
type DownloadOptions = {
blob: Blob;
name: string;
autoDownload?: boolean;
opts?: typeof DEFAULT_STYLES;
};
// set up the iframe as a singleton so we don't have to figure out destruction of it down the line.
@ -81,10 +89,11 @@ export class FileDownloader {
return iframe;
}
public async download({ blob, name, autoDownload = true }: DownloadOptions) {
public async download({ blob, name, autoDownload = true, opts = DEFAULT_STYLES }: DownloadOptions) {
const iframe = this.iframe; // get the iframe first just in case we need to await onload
if (this.onLoadPromise) await this.onLoadPromise;
iframe.contentWindow.postMessage({
...opts,
blob: blob,
download: name,
auto: autoDownload,