From 001ed616f6996c65d8edd55079db138b40245220 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 11 Aug 2025 16:20:21 +0100 Subject: [PATCH] Fix downloading files with authenticated media API (#30520) * Fix downloading files with authenticated media API Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update snapshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/right-panel/file-panel.spec.ts | 18 +++---------- src/components/views/messages/MFileBody.tsx | 26 ++++--------------- .../__snapshots__/MFileBody-test.tsx.snap | 2 -- 3 files changed, 9 insertions(+), 37 deletions(-) diff --git a/playwright/e2e/right-panel/file-panel.spec.ts b/playwright/e2e/right-panel/file-panel.spec.ts index f6d89511b7..5a45beb095 100644 --- a/playwright/e2e/right-panel/file-panel.spec.ts +++ b/playwright/e2e/right-panel/file-panel.spec.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type Download, type Page } from "@playwright/test"; +import { type Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; import { viewRoomSummaryByName } from "./utils"; @@ -189,23 +189,13 @@ test.describe("FilePanel", () => { const link = imageBody.locator(".mx_MFileBody_download a"); - const newPagePromise = context.waitForEvent("page"); - - const downloadPromise = new Promise((resolve) => { - page.once("download", resolve); - }); + const downloadPromise = page.waitForEvent("download"); // Click the anchor link (not the image itself) await link.click(); - const newPage = await newPagePromise; - // XXX: Clicking the link opens the image in a new tab on some browsers rather than downloading - await expect(newPage) - .toHaveURL(/.+\/_matrix\/media\/\w+\/download\/localhost\/\w+/) - .catch(async () => { - const download = await downloadPromise; - expect(download.suggestedFilename()).toBe("riot.png"); - }); + const download = await downloadPromise; + expect(download.suggestedFilename()).toBe("riot.png"); }); }); }); diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index 24b141599e..1c55385d2c 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -178,7 +178,6 @@ export default class MFileBody extends React.Component { public render(): React.ReactNode { const isEncrypted = this.props.mediaEventHelper?.media.isEncrypted; const contentUrl = this.getContentUrl(); - const contentFileSize = this.content.info ? this.content.info.size : null; const fileType = this.content.info?.mimetype ?? "application/octet-stream"; // defaultProps breaks types on IBodyProps, so instead define the default here. const showGenericPlaceholder = this.props.showGenericPlaceholder ?? true; @@ -285,21 +284,9 @@ export default class MFileBody extends React.Component { target: "_blank", rel: "noreferrer 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 contentFileSize === "number" ? contentFileSize > 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) => { + // We cannot rely on href+download to download media due to the authenticated media API as it relies + // on authentication via headers, so we'll have to download the file into memory and then download it. + onClick: (e) => { logger.log(`Downloading ${fileType} as blob (unencrypted)`); // Avoid letting the do its thing @@ -319,11 +306,8 @@ export default class MFileBody extends React.Component { tempAnchor.click(); tempAnchor.remove(); }); - }; - } else { - // Else we are hoping the browser will do the right thing - downloadProps["download"] = this.fileName; - } + }, + }; return ( diff --git a/test/unit-tests/components/views/messages/__snapshots__/MFileBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/MFileBody-test.tsx.snap index a7356697e5..f14bbb08aa 100644 --- a/test/unit-tests/components/views/messages/__snapshots__/MFileBody-test.tsx.snap +++ b/test/unit-tests/components/views/messages/__snapshots__/MFileBody-test.tsx.snap @@ -12,8 +12,6 @@ exports[` should show a download button in file rendering type 1`] = class="_button_vczzf_8 _has-icon_vczzf_57" data-kind="secondary" data-size="sm" - download="alt for a image" - href="https://server/_matrix/media/v3/download/server/image" rel="noreferrer noopener" role="link" tabindex="0"