diff --git a/apps/browser/src/browser/browserApi.ts b/apps/browser/src/browser/browserApi.ts index ad4d7b2788..d3796e78a7 100644 --- a/apps/browser/src/browser/browserApi.ts +++ b/apps/browser/src/browser/browserApi.ts @@ -1,7 +1,3 @@ -import { Utils } from "@bitwarden/common/misc/utils"; - -import { SafariApp } from "./safariApp"; - export class BrowserApi { static isWebExtensionsApi: boolean = typeof browser !== "undefined"; static isSafariApi: boolean = @@ -150,39 +146,6 @@ export class BrowserApi { } } - static downloadFile(win: Window, blobData: any, blobOptions: any, fileName: string) { - if (BrowserApi.isSafariApi) { - const type = blobOptions != null ? blobOptions.type : null; - let data: string = null; - if (type === "text/plain" && typeof blobData === "string") { - data = blobData; - } else { - data = Utils.fromBufferToB64(blobData); - } - SafariApp.sendMessageToApp( - "downloadFile", - JSON.stringify({ - blobData: data, - blobOptions: blobOptions, - fileName: fileName, - }), - true - ); - } else { - const blob = new Blob([blobData], blobOptions); - if (navigator.msSaveOrOpenBlob) { - navigator.msSaveBlob(blob, fileName); - } else { - const a = win.document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = fileName; - win.document.body.appendChild(a); - a.click(); - win.document.body.removeChild(a); - } - } - } - static gaFilter() { return process.env.ENV !== "production"; } diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 62b468e19e..0c60df6c2f 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -18,6 +18,7 @@ import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunc import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; import { EventService } from "@bitwarden/common/abstractions/event.service"; import { ExportService } from "@bitwarden/common/abstractions/export.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { FileUploadService } from "@bitwarden/common/abstractions/fileUpload.service"; import { FolderService } from "@bitwarden/common/abstractions/folder.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; @@ -51,6 +52,7 @@ import MainBackground from "../../background/main.background"; import { BrowserApi } from "../../browser/browserApi"; import { AutofillService } from "../../services/abstractions/autofill.service"; import { StateService as StateServiceAbstraction } from "../../services/abstractions/state.service"; +import { BrowserFileDownloadService } from "../../services/browserFileDownloadService"; import BrowserMessagingService from "../../services/browserMessaging.service"; import BrowserMessagingPrivateModePopupService from "../../services/browserMessagingPrivateModePopup.service"; import { VaultFilterService } from "../../services/vaultFilter.service"; @@ -272,6 +274,10 @@ function getBgService(service: keyof MainBackground) { useExisting: StateServiceAbstraction, deps: [], }, + { + provide: FileDownloadService, + useClass: BrowserFileDownloadService, + }, ], }) export class ServicesModule {} diff --git a/apps/browser/src/popup/settings/export.component.ts b/apps/browser/src/popup/settings/export.component.ts index f049c037ac..c2f9031759 100644 --- a/apps/browser/src/popup/settings/export.component.ts +++ b/apps/browser/src/popup/settings/export.component.ts @@ -6,6 +6,7 @@ import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/compo import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EventService } from "@bitwarden/common/abstractions/event.service"; import { ExportService } from "@bitwarden/common/abstractions/export.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -27,7 +28,8 @@ export class ExportComponent extends BaseExportComponent { private router: Router, logService: LogService, userVerificationService: UserVerificationService, - formBuilder: FormBuilder + formBuilder: FormBuilder, + fileDownloadService: FileDownloadService ) { super( cryptoService, @@ -39,7 +41,8 @@ export class ExportComponent extends BaseExportComponent { window, logService, userVerificationService, - formBuilder + formBuilder, + fileDownloadService ); } diff --git a/apps/browser/src/popup/vault/attachments.component.ts b/apps/browser/src/popup/vault/attachments.component.ts index b36a064a60..ab868f6130 100644 --- a/apps/browser/src/popup/vault/attachments.component.ts +++ b/apps/browser/src/popup/vault/attachments.component.ts @@ -7,6 +7,7 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/ang import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -28,7 +29,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { private location: Location, private route: ActivatedRoute, stateService: StateService, - logService: LogService + logService: LogService, + fileDownloadService: FileDownloadService ) { super( cipherService, @@ -38,7 +40,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { apiService, window, logService, - stateService + stateService, + fileDownloadService ); } diff --git a/apps/browser/src/popup/vault/view.component.ts b/apps/browser/src/popup/vault/view.component.ts index 190a0538b9..87f94ab75b 100644 --- a/apps/browser/src/popup/vault/view.component.ts +++ b/apps/browser/src/popup/vault/view.component.ts @@ -10,6 +10,7 @@ import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.s import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EventService } from "@bitwarden/common/abstractions/event.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; @@ -61,7 +62,8 @@ export class ViewComponent extends BaseViewComponent { private popupUtilsService: PopupUtilsService, apiService: ApiService, passwordRepromptService: PasswordRepromptService, - logService: LogService + logService: LogService, + fileDownloadService: FileDownloadService ) { super( cipherService, @@ -79,7 +81,8 @@ export class ViewComponent extends BaseViewComponent { apiService, passwordRepromptService, logService, - stateService + stateService, + fileDownloadService ); } diff --git a/apps/browser/src/services/browserFileDownloadService.ts b/apps/browser/src/services/browserFileDownloadService.ts new file mode 100644 index 0000000000..29ff661923 --- /dev/null +++ b/apps/browser/src/services/browserFileDownloadService.ts @@ -0,0 +1,46 @@ +import { Injectable } from "@angular/core"; + +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; +import { FileDownloadBuilder } from "@bitwarden/common/abstractions/fileDownload/fileDownloadBuilder"; +import { FileDownloadRequest } from "@bitwarden/common/abstractions/fileDownload/fileDownloadRequest"; +import { Utils } from "@bitwarden/common/misc/utils"; + +import { BrowserApi } from "../browser/browserApi"; +import { SafariApp } from "../browser/safariApp"; + +@Injectable() +export class BrowserFileDownloadService implements FileDownloadService { + download(request: FileDownloadRequest): void { + const builder = new FileDownloadBuilder(request); + if (BrowserApi.isSafariApi) { + let data: BlobPart = null; + if (builder.blobOptions.type === "text/plain" && typeof request.blobData === "string") { + data = request.blobData; + } else { + builder.blob.arrayBuffer().then((buf) => { + data = Utils.fromBufferToB64(buf); + }); + } + SafariApp.sendMessageToApp( + "downloadFile", + JSON.stringify({ + blobData: data, + blobOptions: request.blobOptions, + fileName: request.fileName, + }), + true + ); + } else { + if (navigator.msSaveOrOpenBlob) { + navigator.msSaveBlob(builder.blob, request.fileName); + } else { + const a = window.document.createElement("a"); + a.href = URL.createObjectURL(builder.blob); + a.download = request.fileName; + window.document.body.appendChild(a); + a.click(); + window.document.body.removeChild(a); + } + } + } +} diff --git a/apps/browser/src/services/browserPlatformUtils.service.ts b/apps/browser/src/services/browserPlatformUtils.service.ts index 42784af40e..2cf54eda81 100644 --- a/apps/browser/src/services/browserPlatformUtils.service.ts +++ b/apps/browser/src/services/browserPlatformUtils.service.ts @@ -122,10 +122,6 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService BrowserApi.createNewTab(uri, options && options.extensionPage === true); } - saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void { - BrowserApi.downloadFile(win, blobData, blobOptions, fileName); - } - getApplicationVersion(): Promise { return Promise.resolve(BrowserApi.getApplicationVersion()); } diff --git a/apps/desktop/src/app/services/desktopFileDownloadService.ts b/apps/desktop/src/app/services/desktopFileDownloadService.ts new file mode 100644 index 0000000000..df9be25595 --- /dev/null +++ b/apps/desktop/src/app/services/desktopFileDownloadService.ts @@ -0,0 +1,18 @@ +import { Injectable } from "@angular/core"; + +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; +import { FileDownloadBuilder } from "@bitwarden/common/abstractions/fileDownload/fileDownloadBuilder"; +import { FileDownloadRequest } from "@bitwarden/common/abstractions/fileDownload/fileDownloadRequest"; + +@Injectable() +export class DesktopFileDownloadService implements FileDownloadService { + download(request: FileDownloadRequest): void { + const a = window.document.createElement("a"); + a.href = URL.createObjectURL(new FileDownloadBuilder(request).blob); + a.download = request.fileName; + a.style.position = "fixed"; + window.document.body.appendChild(a); + a.click(); + window.document.body.removeChild(a); + } +} diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 2d7d858473..df32a71839 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -15,6 +15,7 @@ import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractE import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/abstractions/broadcaster.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/abstractions/i18n.service"; import { LogService, @@ -48,6 +49,7 @@ import { LoginGuard } from "../guards/login.guard"; import { SearchBarService } from "../layout/search/search-bar.service"; import { DesktopThemingService } from "./desktop-theming.service"; +import { DesktopFileDownloadService } from "./desktopFileDownloadService"; import { InitService } from "./init.service"; const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK"); @@ -137,6 +139,10 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK"); STATE_SERVICE_USE_CACHE, ], }, + { + provide: FileDownloadService, + useClass: DesktopFileDownloadService, + }, { provide: AbstractThemingService, useClass: DesktopThemingService, diff --git a/apps/desktop/src/app/vault/attachments.component.ts b/apps/desktop/src/app/vault/attachments.component.ts index 5c950c295f..40a3f619ee 100644 --- a/apps/desktop/src/app/vault/attachments.component.ts +++ b/apps/desktop/src/app/vault/attachments.component.ts @@ -4,6 +4,7 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/ang import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -21,7 +22,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { platformUtilsService: PlatformUtilsService, apiService: ApiService, logService: LogService, - stateService: StateService + stateService: StateService, + fileDownloadService: FileDownloadService ) { super( cipherService, @@ -31,7 +33,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { apiService, window, logService, - stateService + stateService, + fileDownloadService ); } } diff --git a/apps/desktop/src/app/vault/export.component.ts b/apps/desktop/src/app/vault/export.component.ts index b1e48abcdd..2448683800 100644 --- a/apps/desktop/src/app/vault/export.component.ts +++ b/apps/desktop/src/app/vault/export.component.ts @@ -8,6 +8,7 @@ import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.s import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EventService } from "@bitwarden/common/abstractions/event.service"; import { ExportService } from "@bitwarden/common/abstractions/export.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -31,7 +32,8 @@ export class ExportComponent extends BaseExportComponent implements OnInit { userVerificationService: UserVerificationService, formBuilder: FormBuilder, private broadcasterService: BroadcasterService, - logService: LogService + logService: LogService, + fileDownloadService: FileDownloadService ) { super( cryptoService, @@ -43,7 +45,8 @@ export class ExportComponent extends BaseExportComponent implements OnInit { window, logService, userVerificationService, - formBuilder + formBuilder, + fileDownloadService ); } diff --git a/apps/desktop/src/app/vault/view.component.ts b/apps/desktop/src/app/vault/view.component.ts index dd0986948e..eb58df0d1e 100644 --- a/apps/desktop/src/app/vault/view.component.ts +++ b/apps/desktop/src/app/vault/view.component.ts @@ -14,6 +14,7 @@ import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.s import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EventService } from "@bitwarden/common/abstractions/event.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; @@ -49,7 +50,8 @@ export class ViewComponent extends BaseViewComponent implements OnChanges { private messagingService: MessagingService, passwordRepromptService: PasswordRepromptService, logService: LogService, - stateService: StateService + stateService: StateService, + fileDownloadService: FileDownloadService ) { super( cipherService, @@ -67,7 +69,8 @@ export class ViewComponent extends BaseViewComponent implements OnChanges { apiService, passwordRepromptService, logService, - stateService + stateService, + fileDownloadService ); } ngOnInit() { diff --git a/apps/web/src/app/common/base.events.component.ts b/apps/web/src/app/common/base.events.component.ts index 6907a3db04..7bc1a94c17 100644 --- a/apps/web/src/app/common/base.events.component.ts +++ b/apps/web/src/app/common/base.events.component.ts @@ -1,6 +1,7 @@ import { Directive } from "@angular/core"; import { ExportService } from "@bitwarden/common/abstractions/export.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -30,7 +31,8 @@ export abstract class BaseEventsComponent { protected i18nService: I18nService, protected exportService: ExportService, protected platformUtilsService: PlatformUtilsService, - protected logService: LogService + protected logService: LogService, + protected fileDownloadService: FileDownloadService ) { const defaultDates = this.eventService.getDefaultDateFilters(); this.start = defaultDates[0]; @@ -173,6 +175,10 @@ export abstract class BaseEventsComponent { const data = await this.exportService.getEventExport(events); const fileName = this.exportService.getFileName(this.exportFileName, "csv"); - this.platformUtilsService.saveFile(window, data, { type: "text/plain" }, fileName); + this.fileDownloadService.download({ + fileName, + blobData: data, + blobOptions: { type: "text/plain" }, + }); } } diff --git a/apps/web/src/app/organizations/manage/events.component.ts b/apps/web/src/app/organizations/manage/events.component.ts index 5d347cb729..888ce33144 100644 --- a/apps/web/src/app/organizations/manage/events.component.ts +++ b/apps/web/src/app/organizations/manage/events.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ExportService } from "@bitwarden/common/abstractions/export.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; @@ -37,9 +38,17 @@ export class EventsComponent extends BaseEventsComponent implements OnInit { logService: LogService, private userNamePipe: UserNamePipe, private organizationService: OrganizationService, - private providerService: ProviderService + private providerService: ProviderService, + fileDownloadService: FileDownloadService ) { - super(eventService, i18nService, exportService, platformUtilsService, logService); + super( + eventService, + i18nService, + exportService, + platformUtilsService, + logService, + fileDownloadService + ); } async ngOnInit() { diff --git a/apps/web/src/app/organizations/settings/download-license.component.ts b/apps/web/src/app/organizations/settings/download-license.component.ts index 8d2383c18b..05927c1ec3 100644 --- a/apps/web/src/app/organizations/settings/download-license.component.ts +++ b/apps/web/src/app/organizations/settings/download-license.component.ts @@ -1,8 +1,8 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @Component({ selector: "app-download-license", @@ -18,7 +18,7 @@ export class DownloadLicenseComponent { constructor( private apiService: ApiService, - private platformUtilsService: PlatformUtilsService, + private fileDownloadService: FileDownloadService, private logService: LogService ) {} @@ -34,12 +34,10 @@ export class DownloadLicenseComponent { ); const license = await this.formPromise; const licenseString = JSON.stringify(license, null, 2); - this.platformUtilsService.saveFile( - window, - licenseString, - null, - "bitwarden_organization_license.json" - ); + this.fileDownloadService.download({ + fileName: "bitwarden_organization_license.json", + blobData: licenseString, + }); this.onDownloaded.emit(); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/organizations/tools/import-export/org-export.component.ts b/apps/web/src/app/organizations/tools/import-export/org-export.component.ts index 14facbea1a..d06165daf2 100644 --- a/apps/web/src/app/organizations/tools/import-export/org-export.component.ts +++ b/apps/web/src/app/organizations/tools/import-export/org-export.component.ts @@ -5,6 +5,7 @@ import { ActivatedRoute } from "@angular/router"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EventService } from "@bitwarden/common/abstractions/event.service"; import { ExportService } from "@bitwarden/common/abstractions/export.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -28,7 +29,8 @@ export class OrganizationExportComponent extends ExportComponent { policyService: PolicyService, logService: LogService, userVerificationService: UserVerificationService, - formBuilder: FormBuilder + formBuilder: FormBuilder, + fileDownloadService: FileDownloadService ) { super( cryptoService, @@ -39,7 +41,8 @@ export class OrganizationExportComponent extends ExportComponent { policyService, logService, userVerificationService, - formBuilder + formBuilder, + fileDownloadService ); } diff --git a/apps/web/src/app/organizations/vault/attachments.component.ts b/apps/web/src/app/organizations/vault/attachments.component.ts index 463cc7e03a..aac10c8d8e 100644 --- a/apps/web/src/app/organizations/vault/attachments.component.ts +++ b/apps/web/src/app/organizations/vault/attachments.component.ts @@ -3,6 +3,7 @@ import { Component } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -29,7 +30,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { stateService: StateService, platformUtilsService: PlatformUtilsService, apiService: ApiService, - logService: LogService + logService: LogService, + fileDownloadService: FileDownloadService ) { super( cipherService, @@ -38,7 +40,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { stateService, platformUtilsService, apiService, - logService + logService, + fileDownloadService ); } diff --git a/apps/web/src/app/send/access.component.ts b/apps/web/src/app/send/access.component.ts index a0a3a081ce..fca9a1deff 100644 --- a/apps/web/src/app/send/access.component.ts +++ b/apps/web/src/app/send/access.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { SEND_KDF_ITERATIONS } from "@bitwarden/common/enums/kdfType"; @@ -44,7 +45,8 @@ export class AccessComponent implements OnInit { private apiService: ApiService, private platformUtilsService: PlatformUtilsService, private route: ActivatedRoute, - private cryptoService: CryptoService + private cryptoService: CryptoService, + private fileDownloadService: FileDownloadService ) {} get sendText() { @@ -109,7 +111,11 @@ export class AccessComponent implements OnInit { try { const buf = await response.arrayBuffer(); const decBuf = await this.cryptoService.decryptFromBytes(buf, this.decKey); - this.platformUtilsService.saveFile(window, decBuf, null, this.send.file.fileName); + this.fileDownloadService.download({ + fileName: this.send.file.fileName, + blobData: decBuf, + downloadMethod: "save", + }); } catch (e) { this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); } diff --git a/apps/web/src/app/services/services.module.ts b/apps/web/src/app/services/services.module.ts index 353c8d907f..b5677f7871 100644 --- a/apps/web/src/app/services/services.module.ts +++ b/apps/web/src/app/services/services.module.ts @@ -11,6 +11,7 @@ import { MEMORY_STORAGE, } from "@bitwarden/angular/services/jslib-services.module"; import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service"; @@ -41,6 +42,7 @@ import { InitService } from "./init.service"; import { ModalService } from "./modal.service"; import { PolicyListService } from "./policy-list.service"; import { RouterService } from "./router.service"; +import { WebFileDownloadService } from "./webFileDownload.service"; @NgModule({ imports: [ToastrModule, JslibServicesModule], @@ -114,6 +116,10 @@ import { RouterService } from "./router.service"; provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService, }, + { + provide: FileDownloadService, + useClass: WebFileDownloadService, + }, HomeGuard, ], }) diff --git a/apps/web/src/app/services/webFileDownload.service.ts b/apps/web/src/app/services/webFileDownload.service.ts new file mode 100644 index 0000000000..de1626c8df --- /dev/null +++ b/apps/web/src/app/services/webFileDownload.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from "@angular/core"; + +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; +import { FileDownloadBuilder } from "@bitwarden/common/abstractions/fileDownload/fileDownloadBuilder"; +import { FileDownloadRequest } from "@bitwarden/common/abstractions/fileDownload/fileDownloadRequest"; +import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; + +@Injectable() +export class WebFileDownloadService implements FileDownloadService { + constructor(private platformUtilsService: PlatformUtilsService) {} + + download(request: FileDownloadRequest): void { + const builder = new FileDownloadBuilder(request); + const a = window.document.createElement("a"); + if (builder.downloadMethod === "save") { + a.download = request.fileName; + } else if (!this.platformUtilsService.isSafari()) { + a.target = "_blank"; + } + a.href = URL.createObjectURL(builder.blob); + a.style.position = "fixed"; + window.document.body.appendChild(a); + a.click(); + window.document.body.removeChild(a); + } +} diff --git a/apps/web/src/app/settings/emergency-access-attachments.component.ts b/apps/web/src/app/settings/emergency-access-attachments.component.ts index dbf19cb5b9..525a332f14 100644 --- a/apps/web/src/app/settings/emergency-access-attachments.component.ts +++ b/apps/web/src/app/settings/emergency-access-attachments.component.ts @@ -4,6 +4,7 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/ang import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -25,7 +26,8 @@ export class EmergencyAccessAttachmentsComponent extends BaseAttachmentsComponen stateService: StateService, platformUtilsService: PlatformUtilsService, apiService: ApiService, - logService: LogService + logService: LogService, + fileDownloadService: FileDownloadService ) { super( cipherService, @@ -35,7 +37,8 @@ export class EmergencyAccessAttachmentsComponent extends BaseAttachmentsComponen apiService, window, logService, - stateService + stateService, + fileDownloadService ); } diff --git a/apps/web/src/app/settings/user-subscription.component.ts b/apps/web/src/app/settings/user-subscription.component.ts index 64ca0589ec..5eb1442bf5 100644 --- a/apps/web/src/app/settings/user-subscription.component.ts +++ b/apps/web/src/app/settings/user-subscription.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { Router } from "@angular/router"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -30,7 +31,8 @@ export class UserSubscriptionComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private router: Router, - private logService: LogService + private logService: LogService, + private fileDownloadService: FileDownloadService ) { this.selfHosted = platformUtilsService.isSelfHost(); } @@ -139,12 +141,10 @@ export class UserSubscriptionComponent implements OnInit { } const licenseString = JSON.stringify(this.sub.license, null, 2); - this.platformUtilsService.saveFile( - window, - licenseString, - null, - "bitwarden_premium_license.json" - ); + this.fileDownloadService.download({ + fileName: "bitwarden_premium_license.json", + blobData: licenseString, + }); } updateLicense() { diff --git a/apps/web/src/app/tools/import-export/export.component.ts b/apps/web/src/app/tools/import-export/export.component.ts index d5e6aadccb..c4737e868b 100644 --- a/apps/web/src/app/tools/import-export/export.component.ts +++ b/apps/web/src/app/tools/import-export/export.component.ts @@ -5,6 +5,7 @@ import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/compo import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EventService } from "@bitwarden/common/abstractions/event.service"; import { ExportService } from "@bitwarden/common/abstractions/export.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -27,7 +28,8 @@ export class ExportComponent extends BaseExportComponent { policyService: PolicyService, logService: LogService, userVerificationService: UserVerificationService, - formBuilder: FormBuilder + formBuilder: FormBuilder, + fileDownloadService: FileDownloadService ) { super( cryptoService, @@ -39,7 +41,8 @@ export class ExportComponent extends BaseExportComponent { window, logService, userVerificationService, - formBuilder + formBuilder, + fileDownloadService ); } diff --git a/apps/web/src/app/vault/attachments.component.ts b/apps/web/src/app/vault/attachments.component.ts index fdf2d9f70e..080c06e37e 100644 --- a/apps/web/src/app/vault/attachments.component.ts +++ b/apps/web/src/app/vault/attachments.component.ts @@ -4,6 +4,7 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/ang import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -24,7 +25,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { stateService: StateService, platformUtilsService: PlatformUtilsService, apiService: ApiService, - logService: LogService + logService: LogService, + fileDownloadService: FileDownloadService ) { super( cipherService, @@ -34,7 +36,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { apiService, window, logService, - stateService + stateService, + fileDownloadService ); } diff --git a/apps/web/src/services/webPlatformUtils.service.ts b/apps/web/src/services/webPlatformUtils.service.ts index 40591841ee..115d53401a 100644 --- a/apps/web/src/services/webPlatformUtils.service.ts +++ b/apps/web/src/services/webPlatformUtils.service.ts @@ -104,54 +104,6 @@ export class WebPlatformUtilsService implements PlatformUtilsService { document.body.removeChild(a); } - saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void { - let blob: Blob = null; - let type: string = null; - const fileNameLower = fileName.toLowerCase(); - let doDownload = true; - if (fileNameLower.endsWith(".pdf")) { - type = "application/pdf"; - doDownload = false; - } else if (fileNameLower.endsWith(".xlsx")) { - type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; - } else if (fileNameLower.endsWith(".docx")) { - type = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; - } else if (fileNameLower.endsWith(".pptx")) { - type = "application/vnd.openxmlformats-officedocument.presentationml.presentation"; - } else if (fileNameLower.endsWith(".csv")) { - type = "text/csv"; - } else if (fileNameLower.endsWith(".png")) { - type = "image/png"; - } else if (fileNameLower.endsWith(".jpg") || fileNameLower.endsWith(".jpeg")) { - type = "image/jpeg"; - } else if (fileNameLower.endsWith(".gif")) { - type = "image/gif"; - } - if (type != null) { - blobOptions = blobOptions || {}; - if (blobOptions.type == null) { - blobOptions.type = type; - } - } - if (blobOptions != null) { - blob = new Blob([blobData], blobOptions); - } else { - blob = new Blob([blobData]); - } - - const a = win.document.createElement("a"); - if (doDownload) { - a.download = fileName; - } else if (!this.isSafari()) { - a.target = "_blank"; - } - a.href = URL.createObjectURL(blob); - a.style.position = "fixed"; - win.document.body.appendChild(a); - a.click(); - win.document.body.removeChild(a); - } - getApplicationVersion(): Promise { return Promise.resolve(process.env.APPLICATION_VERSION || "-"); } diff --git a/bitwarden_license/bit-web/src/app/providers/manage/events.component.ts b/bitwarden_license/bit-web/src/app/providers/manage/events.component.ts index b2a281f228..422b7fae7b 100644 --- a/bitwarden_license/bit-web/src/app/providers/manage/events.component.ts +++ b/bitwarden_license/bit-web/src/app/providers/manage/events.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ExportService } from "@bitwarden/common/abstractions/export.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -34,9 +35,17 @@ export class EventsComponent extends BaseEventsComponent implements OnInit { platformUtilsService: PlatformUtilsService, private router: Router, logService: LogService, - private userNamePipe: UserNamePipe + private userNamePipe: UserNamePipe, + fileDownloadService: FileDownloadService ) { - super(eventService, i18nService, exportService, platformUtilsService, logService); + super( + eventService, + i18nService, + exportService, + platformUtilsService, + logService, + fileDownloadService + ); } async ngOnInit() { diff --git a/libs/angular/src/components/attachments.component.ts b/libs/angular/src/components/attachments.component.ts index 9f684b66ac..2419e01345 100644 --- a/libs/angular/src/components/attachments.component.ts +++ b/libs/angular/src/components/attachments.component.ts @@ -3,6 +3,7 @@ import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -36,7 +37,8 @@ export class AttachmentsComponent implements OnInit { protected apiService: ApiService, protected win: Window, protected logService: LogService, - protected stateService: StateService + protected stateService: StateService, + protected fileDownloadService: FileDownloadService ) {} async ngOnInit() { @@ -171,7 +173,10 @@ export class AttachmentsComponent implements OnInit { ? attachment.key : await this.cryptoService.getOrgKey(this.cipher.organizationId); const decBuf = await this.cryptoService.decryptFromBytes(buf, key); - this.platformUtilsService.saveFile(this.win, decBuf, null, attachment.fileName); + this.fileDownloadService.download({ + fileName: attachment.fileName, + blobData: decBuf, + }); } catch (e) { this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); } diff --git a/libs/angular/src/components/export.component.ts b/libs/angular/src/components/export.component.ts index 353a58dc72..8f9ce16d92 100644 --- a/libs/angular/src/components/export.component.ts +++ b/libs/angular/src/components/export.component.ts @@ -4,6 +4,7 @@ import { FormBuilder } from "@angular/forms"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EventService } from "@bitwarden/common/abstractions/event.service"; import { ExportService } from "@bitwarden/common/abstractions/export.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -40,7 +41,8 @@ export class ExportComponent implements OnInit { protected win: Window, private logService: LogService, private userVerificationService: UserVerificationService, - private formBuilder: FormBuilder + private formBuilder: FormBuilder, + protected fileDownloadService: FileDownloadService ) {} async ngOnInit() { @@ -150,6 +152,10 @@ export class ExportComponent implements OnInit { private downloadFile(csv: string): void { const fileName = this.getFileName(); - this.platformUtilsService.saveFile(this.win, csv, { type: "text/plain" }, fileName); + this.fileDownloadService.download({ + fileName: fileName, + blobData: csv, + blobOptions: { type: "text/plain" }, + }); } } diff --git a/libs/angular/src/components/view.component.ts b/libs/angular/src/components/view.component.ts index 8fe50f23d1..f3ee7c0fee 100644 --- a/libs/angular/src/components/view.component.ts +++ b/libs/angular/src/components/view.component.ts @@ -15,6 +15,7 @@ import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.s import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EventService } from "@bitwarden/common/abstractions/event.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; @@ -76,7 +77,8 @@ export class ViewComponent implements OnDestroy, OnInit { protected apiService: ApiService, protected passwordRepromptService: PasswordRepromptService, private logService: LogService, - protected stateService: StateService + protected stateService: StateService, + protected fileDownloadService: FileDownloadService ) {} ngOnInit() { @@ -373,7 +375,10 @@ export class ViewComponent implements OnDestroy, OnInit { ? attachment.key : await this.cryptoService.getOrgKey(this.cipher.organizationId); const decBuf = await this.cryptoService.decryptFromBytes(buf, key); - this.platformUtilsService.saveFile(this.win, decBuf, null, attachment.fileName); + this.fileDownloadService.download({ + fileName: attachment.fileName, + blobData: decBuf, + }); } catch (e) { this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); } diff --git a/libs/common/src/abstractions/fileDownload/fileDownload.service.ts b/libs/common/src/abstractions/fileDownload/fileDownload.service.ts new file mode 100644 index 0000000000..c382922589 --- /dev/null +++ b/libs/common/src/abstractions/fileDownload/fileDownload.service.ts @@ -0,0 +1,5 @@ +import { FileDownloadRequest } from "./fileDownloadRequest"; + +export abstract class FileDownloadService { + download: (request: FileDownloadRequest) => void; +} diff --git a/libs/common/src/abstractions/fileDownload/fileDownloadBuilder.ts b/libs/common/src/abstractions/fileDownload/fileDownloadBuilder.ts new file mode 100644 index 0000000000..29e54a6e28 --- /dev/null +++ b/libs/common/src/abstractions/fileDownload/fileDownloadBuilder.ts @@ -0,0 +1,50 @@ +import { FileDownloadRequest } from "./fileDownloadRequest"; + +export class FileDownloadBuilder { + get blobOptions(): any { + const options = this._request.blobOptions ?? {}; + if (options.type == null) { + options.type = this.fileType; + } + return options; + } + + get blob(): Blob { + if (this.blobOptions != null) { + return new Blob([this._request.blobData], this.blobOptions); + } else { + return new Blob([this._request.blobData]); + } + } + + get downloadMethod(): "save" | "open" { + if (this._request.downloadMethod != null) { + return this._request.downloadMethod; + } + return this.fileType != "application/pdf" ? "save" : "open"; + } + + private get fileType() { + const fileNameLower = this._request.fileName.toLowerCase(); + if (fileNameLower.endsWith(".pdf")) { + return "application/pdf"; + } else if (fileNameLower.endsWith(".xlsx")) { + return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + } else if (fileNameLower.endsWith(".docx")) { + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + } else if (fileNameLower.endsWith(".pptx")) { + return "application/vnd.openxmlformats-officedocument.presentationml.presentation"; + } else if (fileNameLower.endsWith(".csv")) { + return "text/csv"; + } else if (fileNameLower.endsWith(".png")) { + return "image/png"; + } else if (fileNameLower.endsWith(".jpg") || fileNameLower.endsWith(".jpeg")) { + return "image/jpeg"; + } else if (fileNameLower.endsWith(".gif")) { + return "image/gif"; + } + return null; + } + + constructor(private readonly _request: FileDownloadRequest) {} +} diff --git a/libs/common/src/abstractions/fileDownload/fileDownloadRequest.ts b/libs/common/src/abstractions/fileDownload/fileDownloadRequest.ts new file mode 100644 index 0000000000..a1accb9347 --- /dev/null +++ b/libs/common/src/abstractions/fileDownload/fileDownloadRequest.ts @@ -0,0 +1,6 @@ +export type FileDownloadRequest = { + fileName: string; + blobData: BlobPart; + blobOptions?: BlobPropertyBag; + downloadMethod?: "save" | "open"; +}; diff --git a/libs/common/src/abstractions/platformUtils.service.ts b/libs/common/src/abstractions/platformUtils.service.ts index f27be9664a..c4300d1322 100644 --- a/libs/common/src/abstractions/platformUtils.service.ts +++ b/libs/common/src/abstractions/platformUtils.service.ts @@ -18,7 +18,6 @@ export abstract class PlatformUtilsService { isMacAppStore: () => boolean; isViewOpen: () => Promise; launchUri: (uri: string, options?: any) => void; - saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; getApplicationVersion: () => Promise; supportsWebAuthn: (win: Window) => boolean; supportsDuo: () => boolean; diff --git a/libs/electron/src/services/electronPlatformUtils.service.ts b/libs/electron/src/services/electronPlatformUtils.service.ts index 8a9ddbf5e3..9eade37b77 100644 --- a/libs/electron/src/services/electronPlatformUtils.service.ts +++ b/libs/electron/src/services/electronPlatformUtils.service.ts @@ -83,16 +83,6 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { shell.openExternal(uri); } - saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void { - const blob = new Blob([blobData], blobOptions); - const a = win.document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = fileName; - win.document.body.appendChild(a); - a.click(); - win.document.body.removeChild(a); - } - getApplicationVersion(): Promise { return ipcRenderer.invoke("appVersion"); } diff --git a/libs/node/src/cli/services/cliPlatformUtils.service.ts b/libs/node/src/cli/services/cliPlatformUtils.service.ts index c72cf6c08f..de7349c1a8 100644 --- a/libs/node/src/cli/services/cliPlatformUtils.service.ts +++ b/libs/node/src/cli/services/cliPlatformUtils.service.ts @@ -84,10 +84,6 @@ export class CliPlatformUtilsService implements PlatformUtilsService { } } - saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void { - throw new Error("Not implemented."); - } - getApplicationVersion(): Promise { return Promise.resolve(this.packageJson.version); }