From 87e273252be42dab90d9a33857fe7755f378338b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 5 Jul 2018 14:39:58 -0400 Subject: [PATCH] exporting organization data --- src/abstractions/export.service.ts | 5 +- src/angular/components/export.component.ts | 22 ++- src/services/export.service.ts | 185 +++++++++++++++------ 3 files changed, 152 insertions(+), 60 deletions(-) diff --git a/src/abstractions/export.service.ts b/src/abstractions/export.service.ts index e03ce20644..7f9f27234b 100644 --- a/src/abstractions/export.service.ts +++ b/src/abstractions/export.service.ts @@ -1,4 +1,5 @@ export abstract class ExportService { - getCsv: () => Promise; - getFileName: () => string; + getExport: (format?: 'csv' | 'json') => Promise; + getOrganizationExport: (organizationId: string, format?: 'csv' | 'json') => Promise; + getFileName: (prefix?: string) => string; } diff --git a/src/angular/components/export.component.ts b/src/angular/components/export.component.ts index 964d8f61b7..c509354f57 100644 --- a/src/angular/components/export.component.ts +++ b/src/angular/components/export.component.ts @@ -15,6 +15,7 @@ import { UserService } from '../../abstractions/user.service'; export class ExportComponent { @Output() onSaved = new EventEmitter(); + formPromise: Promise; masterPassword: string; showPassword = false; @@ -36,10 +37,13 @@ export class ExportComponent { const storedKeyHash = await this.cryptoService.getKeyHash(); if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { - const csv = await this.exportService.getCsv(); - this.analytics.eventTrack.next({ action: 'Exported Data' }); - this.downloadFile(csv); - this.saved(); + try { + this.formPromise = this.getExportData(); + const data = await this.formPromise; + this.analytics.eventTrack.next({ action: 'Exported Data' }); + this.downloadFile(data); + this.saved(); + } catch { } } else { this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), this.i18nService.t('invalidMasterPassword')); @@ -56,8 +60,16 @@ export class ExportComponent { this.onSaved.emit(); } + protected getExportData() { + return this.exportService.getExport('csv'); + } + + protected getFileName(prefix?: string) { + return this.exportService.getFileName(prefix); + } + private downloadFile(csv: string): void { - const fileName = this.exportService.getFileName(); + const fileName = this.getFileName(); this.platformUtilsService.saveFile(this.win, csv, { type: 'text/plain' }, fileName); } } diff --git a/src/services/export.service.ts b/src/services/export.service.ts index e62b089c97..add422a052 100644 --- a/src/services/export.service.ts +++ b/src/services/export.service.ts @@ -2,17 +2,26 @@ import * as papa from 'papaparse'; import { CipherType } from '../enums/cipherType'; +import { ApiService } from '../abstractions/api.service'; import { CipherService } from '../abstractions/cipher.service'; import { ExportService as ExportServiceAbstraction } from '../abstractions/export.service'; import { FolderService } from '../abstractions/folder.service'; import { CipherView } from '../models/view/cipherView'; +import { CollectionView } from '../models/view/collectionView'; import { FolderView } from '../models/view/folderView'; -export class ExportService implements ExportServiceAbstraction { - constructor(private folderService: FolderService, private cipherService: CipherService) { } +import { Cipher } from '../models/domain/cipher'; +import { Collection } from '../models/domain/collection'; - async getCsv(): Promise { +import { CipherData } from '../models/data/cipherData'; +import { CollectionData } from '../models/data/collectionData'; + +export class ExportService implements ExportServiceAbstraction { + constructor(private folderService: FolderService, private cipherService: CipherService, + private apiService: ApiService) { } + + async getExport(format: 'csv' | 'json' = 'csv'): Promise { let decFolders: FolderView[] = []; let decCiphers: CipherView[] = []; const promises = []; @@ -43,67 +52,90 @@ export class ExportService implements ExportServiceAbstraction { return; } - const cipher: any = { - folder: c.folderId && foldersMap.has(c.folderId) ? foldersMap.get(c.folderId).name : null, - favorite: c.favorite ? 1 : null, - type: null, - name: c.name, - notes: c.notes, - fields: null, - // Login props - login_uri: null, - login_username: null, - login_password: null, - login_totp: null, - }; - - if (c.fields) { - c.fields.forEach((f: any) => { - if (!cipher.fields) { - cipher.fields = ''; - } else { - cipher.fields += '\n'; - } - - cipher.fields += ((f.name || '') + ': ' + f.value); - }); - } - - switch (c.type) { - case CipherType.Login: - cipher.type = 'login'; - cipher.login_username = c.login.username; - cipher.login_password = c.login.password; - cipher.login_totp = c.login.totp; - - if (c.login.uris) { - cipher.login_uri = []; - c.login.uris.forEach((u) => { - cipher.login_uri.push(u.uri); - }); - } - break; - case CipherType.SecureNote: - cipher.type = 'note'; - break; - default: - return; - } - + const cipher: any = {}; + cipher.folder = c.folderId != null && foldersMap.has(c.folderId) ? foldersMap.get(c.folderId).name : null; + cipher.favorite = c.favorite ? 1 : null; + this.buildCommonCipher(cipher, c); exportCiphers.push(cipher); }); - return papa.unparse(exportCiphers); + if (format === 'csv') { + return papa.unparse(exportCiphers); + } else { + return JSON.stringify(exportCiphers, null, ' '); + } } - getFileName(): string { + async getOrganizationExport(organizationId: string, format: 'csv' | 'json' = 'csv'): Promise { + const decCollections: CollectionView[] = []; + const decCiphers: CipherView[] = []; + const promises = []; + + promises.push(this.apiService.getCollections(organizationId).then((collections) => { + const collectionPromises = []; + if (collections != null && collections.data != null && collections.data.length > 0) { + collections.data.forEach((c) => { + const collection = new Collection(new CollectionData(c)); + collectionPromises.push(collection.decrypt().then((decCol) => { + decCollections.push(decCol); + })); + }); + } + return Promise.all(collectionPromises); + })); + + promises.push(this.apiService.getCiphersOrganization(organizationId).then((ciphers) => { + const cipherPromises = []; + if (ciphers != null && ciphers.data != null && ciphers.data.length > 0) { + ciphers.data.forEach((c) => { + const cipher = new Cipher(new CipherData(c)); + cipherPromises.push(cipher.decrypt().then((decCipher) => { + decCiphers.push(decCipher); + })); + }); + } + return Promise.all(cipherPromises); + })); + + await Promise.all(promises); + + const collectionsMap = new Map(); + decCollections.forEach((c) => { + collectionsMap.set(c.id, c); + }); + + const exportCiphers: any[] = []; + decCiphers.forEach((c) => { + // only export logins and secure notes + if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) { + return; + } + + const cipher: any = {}; + cipher.collections = []; + if (c.collectionIds != null) { + cipher.collections = c.collectionIds.filter((id) => collectionsMap.has(id)) + .map((id) => collectionsMap.get(id).name); + } + this.buildCommonCipher(cipher, c); + exportCiphers.push(cipher); + }); + + if (format === 'csv') { + return papa.unparse(exportCiphers); + } else { + return JSON.stringify(exportCiphers, null, ' '); + } + } + + getFileName(prefix: string = null): string { const now = new Date(); const dateString = now.getFullYear() + '' + this.padNumber(now.getMonth() + 1, 2) + '' + this.padNumber(now.getDate(), 2) + this.padNumber(now.getHours(), 2) + '' + this.padNumber(now.getMinutes(), 2) + this.padNumber(now.getSeconds(), 2); - return 'bitwarden_export_' + dateString + '.csv'; + return 'bitwarden' + (prefix ? ('_' + prefix) : '') + '_export_' + dateString + '.csv'; } private padNumber(num: number, width: number, padCharacter: string = '0'): string { @@ -111,4 +143,51 @@ export class ExportService implements ExportServiceAbstraction { return numString.length >= width ? numString : new Array(width - numString.length + 1).join(padCharacter) + numString; } + + private buildCommonCipher(cipher: any, c: CipherView) { + cipher.type = null; + cipher.name = c.name; + cipher.notes = c.notes; + cipher.fields = null; + // Login props + cipher.login_uri = null; + cipher.login_username = null; + cipher.login_password = null; + cipher.login_totp = null; + + if (c.fields) { + c.fields.forEach((f: any) => { + if (!cipher.fields) { + cipher.fields = ''; + } else { + cipher.fields += '\n'; + } + + cipher.fields += ((f.name || '') + ': ' + f.value); + }); + } + + switch (c.type) { + case CipherType.Login: + cipher.type = 'login'; + cipher.login_username = c.login.username; + cipher.login_password = c.login.password; + cipher.login_totp = c.login.totp; + + if (c.login.uris) { + cipher.login_uri = []; + c.login.uris.forEach((u) => { + cipher.login_uri.push(u.uri); + }); + } + break; + case CipherType.SecureNote: + cipher.type = 'note'; + break; + default: + return; + } + + return cipher; + } }