1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-06 10:33:57 +00:00

[PM-22750] Reimplement fix old attachment logic (#17689)

* [PM-22750] Add upgradeOldCipherAttachment method to CipherService

* [PM-22750] Refactor download attachment component to use signals

* [PM-22750] Better download url handling

* [PM-22750] Cleanup upgradeOldCipherAttachments method

* [PM-22750] Refactor cipher-attachments.component to use Signals and OnPush

* [PM-22750] Use the correct legacy decryption key for attachments without their own content encryption key

* [PM-22750] Add fix attachment button back to attachments component

* [PM-22750] Fix newly added output signals

* [PM-22750] Fix failing test due to signal refactor

* [PM-22750] Update copy
This commit is contained in:
Shane Melton
2025-12-08 09:14:41 -08:00
committed by GitHub
parent f89c9b0f84
commit 9f5dab05a2
12 changed files with 491 additions and 260 deletions

View File

@@ -1,9 +1,10 @@
<button
*ngIf="!isDecryptionFailure"
[bitAction]="download"
bitIconButton="bwi-download"
buttonType="main"
size="small"
type="button"
[label]="'downloadAttachmentName' | i18n: attachment.fileName"
></button>
@if (!isDecryptionFailure()) {
<button
[bitAction]="download"
bitIconButton="bwi-download"
buttonType="main"
size="small"
type="button"
[label]="'downloadAttachmentName' | i18n: attachment().fileName"
></button>
}

View File

@@ -100,8 +100,8 @@ describe("DownloadAttachmentComponent", () => {
beforeEach(() => {
fixture = TestBed.createComponent(DownloadAttachmentComponent);
component = fixture.componentInstance;
component.attachment = attachment;
component.cipher = cipherView;
fixture.componentRef.setInput("attachment", attachment);
fixture.componentRef.setInput("cipher", cipherView);
fixture.detectChanges();
});
@@ -123,7 +123,8 @@ describe("DownloadAttachmentComponent", () => {
});
it("hides download button when the attachment has decryption failure", () => {
component.attachment.fileName = DECRYPT_ERROR;
const decryptFailureAttachment = { ...attachment, fileName: DECRYPT_ERROR };
fixture.componentRef.setInput("attachment", decryptFailureAttachment);
fixture.detectChanges();
expect(fixture.debugElement.query(By.css("button"))).toBeNull();
@@ -156,7 +157,6 @@ describe("DownloadAttachmentComponent", () => {
expect(showToast).toHaveBeenCalledWith({
message: "errorOccurred",
title: null,
variant: "error",
});
});
@@ -172,7 +172,6 @@ describe("DownloadAttachmentComponent", () => {
expect(showToast).toHaveBeenCalledWith({
message: "errorOccurred",
title: null,
variant: "error",
});
});

View File

@@ -1,7 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -17,38 +15,27 @@ import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.v
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { AsyncActionsModule, IconButtonModule, ToastService } from "@bitwarden/components";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "app-download-attachment",
templateUrl: "./download-attachment.component.html",
imports: [AsyncActionsModule, CommonModule, JslibModule, IconButtonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DownloadAttachmentComponent {
/** Attachment to download */
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input({ required: true }) attachment: AttachmentView;
readonly attachment = input.required<AttachmentView>();
/** The cipher associated with the attachment */
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input({ required: true }) cipher: CipherView;
readonly cipher = input.required<CipherView>();
// When in view mode, we will want to check for the master password reprompt
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() checkPwReprompt?: boolean = false;
/** When in view mode, we will want to check for the master password reprompt */
readonly checkPwReprompt = input<boolean>(false);
// Required for fetching attachment data when viewed from cipher via emergency access
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() emergencyAccessId?: EmergencyAccessId;
/** Required for fetching attachment data when viewed from cipher via emergency access */
readonly emergencyAccessId = input<EmergencyAccessId>();
/** When owners/admins can mange all items and when accessing from the admin console, use the admin endpoint */
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() admin?: boolean = false;
/** When owners/admins can manage all items and when accessing from the admin console, use the admin endpoint */
readonly admin = input<boolean>(false);
constructor(
private i18nService: I18nService,
@@ -59,26 +46,36 @@ export class DownloadAttachmentComponent {
private cipherService: CipherService,
) {}
protected get isDecryptionFailure(): boolean {
return this.attachment.fileName === DECRYPT_ERROR;
}
protected readonly isDecryptionFailure = computed(
() => this.attachment().fileName === DECRYPT_ERROR,
);
/** Download the attachment */
download = async () => {
let url: string;
const attachment = this.attachment();
const cipher = this.cipher();
let url: string | undefined;
if (!attachment.id) {
this.toastService.showToast({
variant: "error",
message: this.i18nService.t("errorOccurred"),
});
return;
}
try {
const attachmentDownloadResponse = this.admin
? await this.apiService.getAttachmentDataAdmin(this.cipher.id, this.attachment.id)
const attachmentDownloadResponse = this.admin()
? await this.apiService.getAttachmentDataAdmin(cipher.id, attachment.id)
: await this.apiService.getAttachmentData(
this.cipher.id,
this.attachment.id,
this.emergencyAccessId,
cipher.id,
attachment.id,
this.emergencyAccessId(),
);
url = attachmentDownloadResponse.url;
} catch (e) {
if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) {
url = this.attachment.url;
url = attachment.url;
} else if (e instanceof ErrorResponse) {
throw new Error((e as ErrorResponse).getSingleMessage());
} else {
@@ -86,11 +83,18 @@ export class DownloadAttachmentComponent {
}
}
if (!url) {
this.toastService.showToast({
variant: "error",
message: this.i18nService.t("errorOccurred"),
});
return;
}
const response = await fetch(new Request(url, { cache: "no-store" }));
if (response.status !== 200) {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("errorOccurred"),
});
return;
@@ -99,26 +103,31 @@ export class DownloadAttachmentComponent {
try {
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
if (!userId || !attachment.fileName) {
this.toastService.showToast({
variant: "error",
message: this.i18nService.t("errorOccurred"),
});
return;
}
const decBuf = await this.cipherService.getDecryptedAttachmentBuffer(
this.cipher.id as CipherId,
this.attachment,
cipher.id as CipherId,
attachment,
response,
userId,
// When the emergency access ID is present, the cipher is being viewed via emergency access.
// Force legacy decryption in these cases.
this.emergencyAccessId ? true : false,
Boolean(this.emergencyAccessId()),
);
this.fileDownloadService.download({
fileName: this.attachment.fileName,
fileName: attachment.fileName,
blobData: decBuf,
});
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
} catch {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("errorOccurred"),
});
}