Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 18 additions & 21 deletions apps/browser/src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3460,33 +3460,18 @@
"exportingPersonalVaultTitle": {
"message": "Exporting individual vault"
},
"exportingIndividualVaultDescription": {
"message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
}
}
},
"exportingIndividualVaultWithAttachmentsDescription": {
"message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
}
}
},
"exportingOrganizationVaultTitle": {
"message": "Exporting organization vault"
},
"exportingOrganizationVaultDesc": {
"message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.",
"exportingIndividualVaultScopeDescription": {
"message": "Only items in your individual vault will export."
},
"exportingOrganizationVaultScopeDescription": {
"message": "Only items associated with the $ORGANIZATION$ vault will export. Items in individual vaults will not.",
"placeholders": {
"organization": {
"content": "$1",
"example": "ACME Moving Co."
"example": "My Org Name"
}
}
},
Expand Down Expand Up @@ -4577,6 +4562,18 @@
"exportError": {
"message": "An error occurred while exporting your vault data"
},
"exportSuccessSkippedAttachment": {
"message": "1 corrupted attachment was skipped during export. All other vault data and attachments exported successfully."
},
"exportSuccessSkippedAttachments": {
"message": "$COUNT$ corrupted attachments were skipped during export. All other vault data and attachments exported successfully.",
"placeholders": {
"count": {
"content": "$1",
"example": "3"
}
}
},
Comment on lines +4568 to +4576
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't translate in any correct way :( What's going on with full plural support in web/browser/desktop app?

"typePasskey": {
"message": "Passkey"
},
Expand Down
1 change: 1 addition & 0 deletions apps/browser/src/background/main.background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,7 @@ export default class MainBackground {
this.kdfConfigService,
this.apiService,
this.restrictedItemTypesService,
this.logService,
);

this.exportApiService = new DefaultVaultExportApiService(this.apiService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
(formDisabled)="this.disabled = $event"
(formLoading)="this.loading = $event"
(onSuccessfulExport)="this.onSuccessfulExport($event)"
(skippedAttachmentCountChange)="this.skippedAttachmentCount.set($event)"
></tools-export>

<popup-footer slot="footer">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { Component, signal } from "@angular/core";
import { Router } from "@angular/router";

import { JslibModule } from "@bitwarden/angular/jslib.module";
Expand Down Expand Up @@ -33,10 +33,15 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page
export class ExportBrowserV2Component {
protected disabled = false;
protected loading = false;
protected readonly skippedAttachmentCount = signal(0);

constructor(private router: Router) {}

protected async onSuccessfulExport(organizationId: string): Promise<void> {
// Skip redirect when attachments were skipped so the user can see the warning callout
if (this.skippedAttachmentCount() > 0) {
return;
}
await this.router.navigate(["/tabs/settings"]);
}
}
1 change: 1 addition & 0 deletions apps/cli/src/service-container/service-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,7 @@ export class ServiceContainer {
this.kdfConfigService,
this.apiService,
this.restrictedItemTypesService,
this.logService,
);

this.vaultExportApiService = new DefaultVaultExportApiService(this.apiService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
(formLoading)="this.loading = $event"
(formDisabled)="this.disabled = $event"
(onSuccessfulExport)="this.onSuccessfulExport($event)"
(skippedAttachmentCountChange)="this.skippedAttachmentCount.set($event)"
></tools-export>
</ng-container>
<ng-container bitDialogFooter>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { Component, signal } from "@angular/core";

import { DialogRef, AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
Expand All @@ -21,13 +21,18 @@ import { ExportComponent } from "@bitwarden/vault-export-ui";
export class ExportDesktopComponent {
protected disabled = false;
protected loading = false;
protected readonly skippedAttachmentCount = signal(0);

constructor(public dialogRef: DialogRef) {}

/**
* Callback that is called after a successful export.
*/
protected async onSuccessfulExport(organizationId: string): Promise<void> {
// Skip closing dialog when attachments were skipped so the user can see the warning callout
if (this.skippedAttachmentCount() > 0) {
return;
}
await this.dialogRef.close();
}
}
39 changes: 18 additions & 21 deletions apps/desktop/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3025,33 +3025,18 @@
"exportingPersonalVaultTitle": {
"message": "Exporting individual vault"
},
"exportingIndividualVaultDescription": {
"message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
}
}
},
"exportingIndividualVaultWithAttachmentsDescription": {
"message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
}
}
},
"exportingOrganizationVaultTitle": {
"message": "Exporting organization vault"
},
"exportingOrganizationVaultDesc": {
"message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.",
"exportingIndividualVaultScopeDescription": {
"message": "Only items in your individual vault will export."
},
"exportingOrganizationVaultScopeDescription": {
"message": "Only items associated with the $ORGANIZATION$ vault will export. Items in individual vaults will not.",
"placeholders": {
"organization": {
"content": "$1",
"example": "ACME Moving Co."
"example": "My Org Name"
}
}
},
Expand Down Expand Up @@ -4010,6 +3995,18 @@
"exportError": {
"message": "An error occurred while exporting your vault data"
},
"exportSuccessSkippedAttachment": {
"message": "1 corrupted attachment was skipped during export. All other vault data and attachments exported successfully."
},
"exportSuccessSkippedAttachments": {
"message": "$COUNT$ corrupted attachments were skipped during export. All other vault data and attachments exported successfully.",
"placeholders": {
"count": {
"content": "$1",
"example": "3"
}
}
},
"multifactorAuthenticationCancelled": {
"message": "Multifactor authentication cancelled"
},
Expand Down
35 changes: 16 additions & 19 deletions apps/web/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2255,6 +2255,18 @@
"exportError": {
"message": "An error occurred while exporting your vault data"
},
"exportSuccessSkippedAttachment": {
"message": "1 corrupted attachment was skipped during export. All other vault data and attachments exported successfully."
},
"exportSuccessSkippedAttachments": {
"message": "$COUNT$ corrupted attachments were skipped during export. All other vault data and attachments exported successfully.",
"placeholders": {
"count": {
"content": "$1",
"example": "3"
}
}
},
"passwordGenerator": {
"message": "Password generator"
},
Expand Down Expand Up @@ -8054,26 +8066,11 @@
"exportingOrganizationVaultTitle": {
"message": "Exporting organization vault"
},
"exportingIndividualVaultDescription": {
"message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
}
}
},
"exportingIndividualVaultWithAttachmentsDescription": {
"message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
}
}
"exportingIndividualVaultScopeDescription": {
"message": "Only items in your individual vault will export."
},
"exportingOrganizationVaultDesc": {
"message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.",
"exportingOrganizationVaultScopeDescription": {
"message": "Only items associated with the $ORGANIZATION$ vault will export. Items in individual vaults will not.",
"placeholders": {
"organization": {
"content": "$1",
Expand Down
1 change: 1 addition & 0 deletions libs/angular/src/services/jslib-services.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,7 @@ const safeProviders: SafeProvider[] = [
KdfConfigService,
ApiServiceAbstraction,
RestrictedItemTypesService,
LogService,
],
}),
safeProvider({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
EncString,
} from "@bitwarden/common/key-management/crypto/models/enc-string";
import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherId, emptyGuid, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
Expand Down Expand Up @@ -175,6 +176,7 @@ describe("VaultExportService", () => {
let apiService: MockProxy<ApiService>;
let restrictedSubject: BehaviorSubject<RestrictedCipherType[]>;
let restrictedItemTypesService: Partial<RestrictedItemTypesService>;
let logService: MockProxy<LogService>;
let fetchMock: jest.Mock;

const userId = emptyGuid as UserId;
Expand All @@ -188,6 +190,7 @@ describe("VaultExportService", () => {
encryptService = mock<EncryptService>();
kdfConfigService = mock<KdfConfigService>();
apiService = mock<ApiService>();
logService = mock<LogService>();

keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any));
restrictedSubject = new BehaviorSubject<RestrictedCipherType[]>([]);
Expand Down Expand Up @@ -225,6 +228,7 @@ describe("VaultExportService", () => {
kdfConfigService,
apiService,
restrictedItemTypesService as RestrictedItemTypesService,
logService,
);
});

Expand Down Expand Up @@ -345,7 +349,7 @@ describe("VaultExportService", () => {
});

it.each([[400], [401], [404], [500]])(
"throws error if the http request fails (status === %n)",
"returns skipped attachment if the http request fails (status === %n)",
async (status) => {
const cipherData = new CipherData();
cipherData.id = "mock-id";
Expand All @@ -365,13 +369,13 @@ describe("VaultExportService", () => {
) as any;
global.Request = jest.fn(() => {}) as any;

await expect(async () => {
await exportService.getExport(userId, "zip");
}).rejects.toThrow("Error downloading attachment");
const result = await exportService.getExport(userId, "zip");
const blobResult = result as ExportedVaultAsBlob;
expect(blobResult.skippedAttachmentCount).toBe(1);
},
);

it("throws error if decrypting attachment fails", async () => {
it("returns skipped attachment if decrypting attachment fails", async () => {
const cipherData = new CipherData();
cipherData.id = "mock-id";
const cipherView = new CipherView(new Cipher(cipherData));
Expand All @@ -393,9 +397,33 @@ describe("VaultExportService", () => {
) as any;
global.Request = jest.fn(() => {}) as any;

await expect(async () => {
await exportService.getExport(userId, "zip");
}).rejects.toThrow("Error decrypting attachment");
const result = await exportService.getExport(userId, "zip");
const blobResult = result as ExportedVaultAsBlob;
expect(blobResult.skippedAttachmentCount).toBe(1);
});

it("returns no skippedAttachments when all attachments succeed", async () => {
const cipherData = new CipherData();
cipherData.id = "mock-id";
const cipherView = new CipherView(new Cipher(cipherData));
const attachmentView = new AttachmentView(new Attachment(new AttachmentData()));
attachmentView.fileName = "mock-file-name";
cipherView.attachments = [attachmentView];

cipherService.getAllDecrypted.mockResolvedValue([cipherView]);
folderService.getAllDecryptedFromState.mockResolvedValue([]);
cipherService.getDecryptedAttachmentBuffer.mockResolvedValue(new Uint8Array(255));
global.fetch = jest.fn(() =>
Promise.resolve({
status: 200,
arrayBuffer: () => Promise.resolve(new ArrayBuffer(255)),
}),
) as any;
global.Request = jest.fn(() => {}) as any;

const result = await exportService.getExport(userId, "zip");
const blobResult = result as ExportedVaultAsBlob;
expect(blobResult.skippedAttachmentCount).toBeUndefined();
});

it("contains attachments with folders", async () => {
Expand Down
Loading
Loading