From 859f317d59189d223072a406bc2d6924e1fb71bc Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 1 Feb 2021 09:44:30 -0600 Subject: [PATCH] [Send] Port web based components (#254) * Initial port of web based send components * Updated import order to satisfy linter --- .../components/send/add-edit.component.ts | 231 ++++++++++++++++++ src/angular/components/send/send.component.ts | 195 +++++++++++++++ 2 files changed, 426 insertions(+) create mode 100644 src/angular/components/send/add-edit.component.ts create mode 100644 src/angular/components/send/send.component.ts diff --git a/src/angular/components/send/add-edit.component.ts b/src/angular/components/send/add-edit.component.ts new file mode 100644 index 0000000000..fb90fa9762 --- /dev/null +++ b/src/angular/components/send/add-edit.component.ts @@ -0,0 +1,231 @@ +import { DatePipe } from '@angular/common'; + +import { + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; + +import { SendType } from '../../../enums/sendType'; + +import { EnvironmentService } from '../../../abstractions/environment.service'; +import { I18nService } from '../../../abstractions/i18n.service'; +import { MessagingService } from '../../../abstractions/messaging.service'; +import { PlatformUtilsService } from '../../../abstractions/platformUtils.service'; +import { SendService } from '../../../abstractions/send.service'; +import { UserService } from '../../../abstractions/user.service'; + +import { SendFileView } from '../../../models/view/sendFileView'; +import { SendTextView } from '../../../models/view/sendTextView'; +import { SendView } from '../../../models/view/sendView'; + +import { Send } from '../../../models/domain/send'; + +export class AddEditComponent implements OnInit { + @Input() sendId: string; + @Input() type: SendType; + + @Output() onSavedSend = new EventEmitter(); + @Output() onDeletedSend = new EventEmitter(); + @Output() onCancelled = new EventEmitter(); + + editMode: boolean = false; + send: SendView; + link: string; + title: string; + deletionDate: string; + expirationDate: string; + hasPassword: boolean; + password: string; + formPromise: Promise; + deletePromise: Promise; + sendType = SendType; + typeOptions: any[]; + deletionDateOptions: any[]; + expirationDateOptions: any[]; + deletionDateSelect = 168; + expirationDateSelect: number = null; + canAccessPremium = true; + premiumRequiredAlertShown = false; + + constructor(protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + protected environmentService: EnvironmentService, protected datePipe: DatePipe, + protected sendService: SendService, protected userService: UserService, + protected messagingService: MessagingService) { + this.typeOptions = [ + { name: i18nService.t('sendTypeFile'), value: SendType.File }, + { name: i18nService.t('sendTypeText'), value: SendType.Text }, + ]; + this.deletionDateOptions = this.expirationDateOptions = [ + { name: i18nService.t('oneHour'), value: 1 }, + { name: i18nService.t('oneDay'), value: 24 }, + { name: i18nService.t('days', '2'), value: 48 }, + { name: i18nService.t('days', '3'), value: 72 }, + { name: i18nService.t('days', '7'), value: 168 }, + { name: i18nService.t('days', '30'), value: 720 }, + { name: i18nService.t('custom'), value: 0 }, + ]; + this.expirationDateOptions = [ + { name: i18nService.t('never'), value: null }, + ].concat([...this.deletionDateOptions]); + } + + async ngOnInit() { + await this.load(); + } + + async load() { + this.editMode = this.sendId != null; + if (this.editMode) { + this.editMode = true; + this.title = this.i18nService.t('editSend'); + } else { + this.title = this.i18nService.t('createSend'); + } + + this.canAccessPremium = await this.userService.canAccessPremium(); + if (!this.canAccessPremium) { + this.type = SendType.Text; + } + + if (this.send == null) { + if (this.editMode) { + const send = await this.loadSend(); + this.send = await send.decrypt(); + } else { + this.send = new SendView(); + this.send.type = this.type == null ? SendType.File : this.type; + this.send.file = new SendFileView(); + this.send.text = new SendTextView(); + this.send.deletionDate = new Date(); + this.send.deletionDate.setDate(this.send.deletionDate.getDate() + 7); + } + } + + this.hasPassword = this.send.password != null && this.send.password.trim() !== ''; + + // Parse dates + this.deletionDate = this.dateToString(this.send.deletionDate); + this.expirationDate = this.dateToString(this.send.expirationDate); + + if (this.editMode) { + let webVaultUrl = this.environmentService.getWebVaultUrl(); + if (webVaultUrl == null) { + webVaultUrl = 'https://vault.bitwarden.com'; + } + this.link = webVaultUrl + '/#/send/' + this.send.accessId + '/' + this.send.urlB64Key; + } + } + + async submit(): Promise { + if (this.send.name == null || this.send.name === '') { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('nameRequired')); + return false; + } + + let file: File = null; + if (this.send.type === SendType.File && !this.editMode) { + const fileEl = document.getElementById('file') as HTMLInputElement; + const files = fileEl.files; + if (files == null || files.length === 0) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('selectFile')); + return; + } + + file = files[0]; + if (file.size > 104857600) { // 100 MB + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('maxFileSize')); + return; + } + } + + if (!this.editMode) { + const now = new Date(); + if (this.deletionDateSelect > 0) { + const d = new Date(); + d.setHours(now.getHours() + this.deletionDateSelect); + this.deletionDate = this.dateToString(d); + } + if (this.expirationDateSelect != null && this.expirationDateSelect > 0) { + const d = new Date(); + d.setHours(now.getHours() + this.expirationDateSelect); + this.expirationDate = this.dateToString(d); + } + } + + const encSend = await this.encryptSend(file); + try { + this.formPromise = this.sendService.saveWithServer(encSend); + await this.formPromise; + this.send.id = encSend[0].id; + this.platformUtilsService.showToast('success', null, + this.i18nService.t(this.editMode ? 'editedSend' : 'createdSend')); + this.onSavedSend.emit(this.send); + return true; + } catch { } + + return false; + } + + clearExpiration() { + this.expirationDate = null; + } + + async delete(): Promise { + if (this.deletePromise != null) { + return; + } + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('deleteSendConfirmation'), + this.i18nService.t('deleteSend'), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return; + } + + try { + this.deletePromise = this.sendService.deleteWithServer(this.send.id); + await this.deletePromise; + this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend')); + await this.load(); + this.onDeletedSend.emit(this.send); + } catch { } + } + + typeChanged() { + if (!this.canAccessPremium && this.send.type === SendType.File && !this.premiumRequiredAlertShown) { + this.premiumRequiredAlertShown = true; + this.messagingService.send('premiumRequired'); + } + } + + protected async loadSend(): Promise { + return this.sendService.get(this.sendId); + } + + protected async encryptSend(file: File): Promise<[Send, ArrayBuffer]> { + const sendData = await this.sendService.encrypt(this.send, file, this.password, null); + + // Parse dates + try { + sendData[0].deletionDate = this.deletionDate == null ? null : new Date(this.deletionDate); + } catch { + sendData[0].deletionDate = null; + } + try { + sendData[0].expirationDate = this.expirationDate == null ? null : new Date(this.expirationDate); + } catch { + sendData[0].expirationDate = null; + } + + return sendData; + } + + protected dateToString(d: Date) { + return d == null ? null : this.datePipe.transform(d, 'yyyy-MM-ddTHH:mm'); + } +} diff --git a/src/angular/components/send/send.component.ts b/src/angular/components/send/send.component.ts new file mode 100644 index 0000000000..2d250f2ff4 --- /dev/null +++ b/src/angular/components/send/send.component.ts @@ -0,0 +1,195 @@ +import { + NgZone, + OnInit, +} from '@angular/core'; + +import { SendType } from '../../../enums/sendType'; + +import { SendView } from '../../../models/view/sendView'; + +import { EnvironmentService } from '../../../abstractions/environment.service'; +import { I18nService } from '../../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../../abstractions/platformUtils.service'; +import { SendService } from '../../../abstractions/send.service'; + +import { BroadcasterService } from '../../../angular/services/broadcaster.service'; + +const BroadcasterSubscriptionId = 'SendComponent'; + +export class SendComponent implements OnInit { + + sendType = SendType; + loaded = false; + loading = true; + refreshing = false; + expired: boolean = false; + type: SendType = null; + sends: SendView[] = []; + filteredSends: SendView[] = []; + searchText: string; + selectedType: SendType; + selectedAll: boolean; + searchPlaceholder: string; + filter: (cipher: SendView) => boolean; + searchPending = false; + + actionPromise: any; + onSuccessfulRemovePassword: () => Promise; + onSuccessfulDelete: () => Promise; + + private searchTimeout: any; + + constructor(protected sendService: SendService, protected i18nService: I18nService, + protected platformUtilsService: PlatformUtilsService, protected environmentService: EnvironmentService, + protected broadcasterService: BroadcasterService, protected ngZone: NgZone) { } + + async ngOnInit() { + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case 'syncCompleted': + if (message.successfully) { + await this.load(); + } + break; + } + }); + }); + + await this.load(); + } + + ngOnDestroy() { + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + } + + async load(filter: (send: SendView) => boolean = null) { + this.loading = true; + const sends = await this.sendService.getAllDecrypted(); + this.sends = sends; + this.selectAll(); + this.loading = false; + this.loaded = true; + } + + async reload(filter: (send: SendView) => boolean = null) { + this.loaded = false; + this.sends = []; + await this.load(filter); + } + + async refresh() { + try { + this.refreshing = true; + await this.reload(this.filter); + } finally { + this.refreshing = false; + } + } + + async applyFilter(filter: (send: SendView) => boolean = null) { + this.filter = filter; + await this.search(null); + } + + async search(timeout: number = null) { + this.searchPending = false; + if (this.searchTimeout != null) { + clearTimeout(this.searchTimeout); + } + if (timeout == null) { + this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); + return; + } + this.searchPending = true; + this.searchTimeout = setTimeout(async () => { + this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); + this.searchPending = false; + }, timeout); + } + + async removePassword(s: SendView): Promise { + if (this.actionPromise != null || s.password == null) { + return; + } + const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('removePasswordConfirmation'), + this.i18nService.t('removePassword'), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return false; + } + + try { + this.actionPromise = this.sendService.removePasswordWithServer(s.id); + await this.actionPromise; + if (this.onSuccessfulRemovePassword() != null) { + this.onSuccessfulRemovePassword(); + } else { + // Default actions + this.platformUtilsService.showToast('success', null, this.i18nService.t('removedPassword')); + await this.load(); + } + } catch { } + this.actionPromise = null; + } + + async delete(s: SendView): Promise { + if (this.actionPromise != null) { + return false; + } + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('deleteSendConfirmation'), + this.i18nService.t('deleteSend'), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return false; + } + + try { + this.actionPromise = this.sendService.deleteWithServer(s.id); + await this.actionPromise; + + if (this.onSuccessfulDelete() != null) { + this.onSuccessfulDelete(); + } else { + // Default actions + this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend')); + await this.load(); + } + } catch { } + this.actionPromise = null; + return true; + } + + copy(s: SendView) { + let webVaultUrl = this.environmentService.getWebVaultUrl(); + if (webVaultUrl == null) { + webVaultUrl = 'https://vault.bitwarden.com'; + } + const link = webVaultUrl + '/#/send/' + s.accessId + '/' + s.urlB64Key; + this.platformUtilsService.copyToClipboard(link); + this.platformUtilsService.showToast('success', null, + this.i18nService.t('valueCopied', this.i18nService.t('sendLink'))); + } + + searchTextChanged() { + this.search(200); + } + + selectAll() { + this.clearSelections(); + this.selectedAll = true; + this.applyFilter(null); + } + + selectType(type: SendType) { + this.clearSelections(); + this.selectedType = type; + this.applyFilter((s) => s.type === type); + } + + clearSelections() { + this.selectedAll = false; + this.selectedType = null; + } +}