[fix] Force send attachment to always download and never open (#2908)

* [refactor] Introduce a file download service

* [refactor] Point platformUtilsService.saveFile() callers to fileDownloadService.download() instead

* [refactor] Remove platformUtilsService.saveFile()

* [fix] Force send attachments to always download and never open

* [fix] Remove the window property from FileDownloadRequest

* [fix] Move FileDownloadRequest to /abstractions/fileDownload

* [fix] Simplify FileDownloadRequest to a type

* [fix] Move BrowserApi.saveFile logic into BrowserFileDownloadService

* [fix] Use proper blob types for file downloads

* [fix] forceDownload -> downloadMethod on FileDownloadRequest

* [fix] Remove fileType from FileDownloadRequest

* [fix] Make fileType private
This commit is contained in:
Addison Beck 2022-06-29 14:15:29 -07:00 committed by GitHub
parent a89b745f0b
commit bb7dce031c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 297 additions and 155 deletions

View File

@ -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";
}

View File

@ -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<T>(service: keyof MainBackground) {
useExisting: StateServiceAbstraction,
deps: [],
},
{
provide: FileDownloadService,
useClass: BrowserFileDownloadService,
},
],
})
export class ServicesModule {}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<string> {
return Promise.resolve(BrowserApi.getApplicationVersion());
}

View File

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

View File

@ -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,

View File

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

View File

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

View File

@ -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() {

View File

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

View File

@ -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() {

View File

@ -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);

View File

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

View File

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

View File

@ -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"));
}

View File

@ -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,
],
})

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

@ -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<string> {
return Promise.resolve(process.env.APPLICATION_VERSION || "-");
}

View File

@ -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() {

View File

@ -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"));
}

View File

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

View File

@ -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"));
}

View File

@ -0,0 +1,5 @@
import { FileDownloadRequest } from "./fileDownloadRequest";
export abstract class FileDownloadService {
download: (request: FileDownloadRequest) => void;
}

View File

@ -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) {}
}

View File

@ -0,0 +1,6 @@
export type FileDownloadRequest = {
fileName: string;
blobData: BlobPart;
blobOptions?: BlobPropertyBag;
downloadMethod?: "save" | "open";
};

View File

@ -18,7 +18,6 @@ export abstract class PlatformUtilsService {
isMacAppStore: () => boolean;
isViewOpen: () => Promise<boolean>;
launchUri: (uri: string, options?: any) => void;
saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void;
getApplicationVersion: () => Promise<string>;
supportsWebAuthn: (win: Window) => boolean;
supportsDuo: () => boolean;

View File

@ -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<string> {
return ipcRenderer.invoke("appVersion");
}

View File

@ -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<string> {
return Promise.resolve(this.packageJson.version);
}