<commit_before>/*
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { createRef } from 'react';
import filesize from 'filesize';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import AccessibleButton from "../elements/AccessibleButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromContent } from "../../../customisations/Media";
import ErrorDialog from "../dialogs/ErrorDialog";
import { TileShape } from "../rooms/EventTile";
import { presentableTextForFile } from "../../../utils/FileUtils";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
import { IBodyProps } from "./IBodyProps";
import { FileDownloader } from "../../../utils/FileDownloader";
import TextWithTooltip from "../elements/TextWithTooltip";

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(require("../../../../res/img/download.svg")).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
// client. Otherwise those scripts written by remote users can read
// the access token and end-to-end keys that are in local storage.
//
// For attachments downloaded directly from the homeserver we can use
// Content-Security-Policy headers to disable script execution.
//
// But attachments with end-to-end encryption are more difficult to handle.
// We need to decrypt the attachment on the client and then display it.
// To display the attachment we need to turn the decrypted bytes into a URL.
//
// There are two ways to turn bytes into URLs, data URL and blob URLs.
// Data URLs aren't suitable for downloading a file because Chrome has a
// 2MB limit on the size of URLs that can be viewed in the browser or
// downloaded. This limit does not seem to apply when the url is used as
// the source attribute of an image tag.
//
// Blob URLs are generated using window.URL.createObjectURL and unfortunately
// for our purposes they inherit the origin of the page that created them.
// This means that any scripts that run when the URL is viewed will be able
// to access local storage.
//
// The easiest solution is to host the code that generates the blob URL on
// a different domain to the client.
// Another possibility is to generate the blob URL within a sandboxed iframe.
// The downside of using a second domain is that it complicates hosting,
// 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;
}

interface IState {
    decryptedBlob?: Blob;
}

@replaceableComponent("views.messages.MFileBody")
export default class MFileBody extends React.Component<IProps, IState> {
    static defaultProps = {
        showGenericPlaceholder: true,
    };

    private iframe: React.RefObject<HTMLIFrameElement> = createRef();
    private dummyLink: React.RefObject<HTMLAnchorElement> = createRef();
    private userDidClick = false;
    private fileDownloader: FileDownloader = new FileDownloader(() => this.iframe.current);

    public constructor(props: IProps) {
        super(props);

        this.state = {};
    }

    private get content(): IMediaEventContent {
        return this.props.mxEvent.getContent<IMediaEventContent>();
    }

    private get fileName(): string {
        return this.content.body && this.content.body.length > 0 ? this.content.body : _t("Attachment");
    }

    private get linkText(): string {
        return presentableTextForFile(this.content);
    }

    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 }),
            },
        });
    }

    private getContentUrl(): string {
        const media = mediaFromContent(this.props.mxEvent.getContent());
        return media.srcHttp;
    }

    public componentDidUpdate(prevProps, prevState) {
        if (this.props.onHeightChanged && !prevState.decryptedBlob && this.state.decryptedBlob) {
            this.props.onHeightChanged();
        }
    }

    private decryptFile = async (): Promise<void> => {
        if (this.state.decryptedBlob) {
            return;
        }
        try {
            this.userDidClick = true;
            this.setState({
                decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value,
            });
        } catch (err) {
            console.warn("Unable to decrypt attachment: ", err);
            Modal.createTrackedDialog('Error decrypting attachment', '', ErrorDialog, {
                title: _t("Error"),
                description: _t("Error decrypting attachment"),
            });
        }
    };

    private onPlaceholderClick = async () => {
        const mediaHelper = this.props.mediaEventHelper;
        if (mediaHelper.media.isEncrypted) {
            await this.decryptFile();
            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.
            this.fileDownloader.download({
                blob: await mediaHelper.sourceBlob.value,
                name: this.fileName,
            });
        }
    };

    public render() {
        const isEncrypted = this.props.mediaEventHelper.media.isEncrypted;
        const contentUrl = this.getContentUrl();
        const fileSize = this.content.info ? this.content.info.size : null;
        const fileType = this.content.info ? this.content.info.mimetype : "application/octet-stream";
<commit_msg>Fix error on accessing encrypted media without keys<commit_after>/*
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { createRef } from 'react';
import filesize from 'filesize';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import AccessibleButton from "../elements/AccessibleButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromContent } from "../../../customisations/Media";
import ErrorDialog from "../dialogs/ErrorDialog";
import { TileShape } from "../rooms/EventTile";
import { presentableTextForFile } from "../../../utils/FileUtils";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
import { IBodyProps } from "./IBodyProps";
import { FileDownloader } from "../../../utils/FileDownloader";
import TextWithTooltip from "../elements/TextWithTooltip";

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(require("../../../../res/img/download.svg")).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
// client. Otherwise those scripts written by remote users can read
// the access token and end-to-end keys that are in local storage.
//
// For attachments downloaded directly from the homeserver we can use
// Content-Security-Policy headers to disable script execution.
//
// But attachments with end-to-end encryption are more difficult to handle.
// We need to decrypt the attachment on the client and then display it.
// To display the attachment we need to turn the decrypted bytes into a URL.
//
// There are two ways to turn bytes into URLs, data URL and blob URLs.
// Data URLs aren't suitable for downloading a file because Chrome has a
// 2MB limit on the size of URLs that can be viewed in the browser or
// downloaded. This limit does not seem to apply when the url is used as
// the source attribute of an image tag.
//
// Blob URLs are generated using window.URL.createObjectURL and unfortunately
// for our purposes they inherit the origin of the page that created them.
// This means that any scripts that run when the URL is viewed will be able
// to access local storage.
//
// The easiest solution is to host the code that generates the blob URL on
// a different domain to the client.
// Another possibility is to generate the blob URL within a sandboxed iframe.
// The downside of using a second domain is that it complicates hosting,
// 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;
}

interface IState {
    decryptedBlob?: Blob;
}

@replaceableComponent("views.messages.MFileBody")
export default class MFileBody extends React.Component<IProps, IState> {
    static defaultProps = {
        showGenericPlaceholder: true,
    };

    private iframe: React.RefObject<HTMLIFrameElement> = createRef();
    private dummyLink: React.RefObject<HTMLAnchorElement> = createRef();
    private userDidClick = false;
    private fileDownloader: FileDownloader = new FileDownloader(() => this.iframe.current);

    public constructor(props: IProps) {
        super(props);

        this.state = {};
    }

    private get content(): IMediaEventContent {
        return this.props.mxEvent.getContent<IMediaEventContent>();
    }

    private get fileName(): string {
        return this.content.body && this.content.body.length > 0 ? this.content.body : _t("Attachment");
    }

    private get linkText(): string {
        return presentableTextForFile(this.content);
    }

    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 }),
            },
        });
    }

    private getContentUrl(): string {
        const media = mediaFromContent(this.props.mxEvent.getContent());
        return media.srcHttp;
    }

    public componentDidUpdate(prevProps, prevState) {
        if (this.props.onHeightChanged && !prevState.decryptedBlob && this.state.decryptedBlob) {
            this.props.onHeightChanged();
        }
    }

    private decryptFile = async (): Promise<void> => {
        if (this.state.decryptedBlob) {
            return;
        }
        try {
            this.userDidClick = true;
            this.setState({
                decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value,
            });
        } catch (err) {
            console.warn("Unable to decrypt attachment: ", err);
            Modal.createTrackedDialog('Error decrypting attachment', '', ErrorDialog, {
                title: _t("Error"),
                description: _t("Error decrypting attachment"),
            });
        }
    };

    private onPlaceholderClick = async () => {
        const mediaHelper = this.props.mediaEventHelper;
        if (mediaHelper?.media.isEncrypted) {
            await this.decryptFile();
            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.
            this.fileDownloader.download({
                blob: await mediaHelper.sourceBlob.value,
                name: this.fileName,
            });
        }
    };

    public render() {
        const isEncrypted = this.props.mediaEventHelper?.media.isEncrypted;
        const contentUrl = this.getContentUrl();
        const fileSize = this.content.info ? this.content.info.size : null;
        const fileType = this.content.info ? this.content.info.mimetype : "application/octet-stream";

{'fraction_non_alphanumeric': 0.10365465427225264, 'fraction_numerical': 0.0029288170126066473, 'mean_word_length': 2.812378640776699, 'pattern_counts': {'":': 2, '<': 15, '<?xml version=': 0, '>': 23, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 46, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3390064', 'n_tokens_mistral': 4212, 'n_tokens_neox': 4091, 'n_words': 1453}