From 42bb8cd5f8f3bece607add3c42610e128984116b Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Tue, 21 Dec 2021 14:48:22 -0500 Subject: [PATCH] Run prettier --- src/background/idle.background.ts | 107 +- src/background/main.background.ts | 1623 +++++++++-------- src/background/nativeMessaging.background.ts | 595 +++--- src/background/notification.background.ts | 866 ++++----- src/background/runtime.background.ts | 419 +++-- src/models/account.ts | 34 +- src/models/browserComponentState.ts | 4 +- src/models/browserGroupingsComponentState.ts | 24 +- src/models/browserSendComponentState.ts | 10 +- src/popup/accounts/home.component.ts | 91 +- src/popup/accounts/lock.component.ts | 148 +- src/popup/accounts/login.component.ts | 88 +- src/popup/accounts/set-password.component.ts | 118 +- src/popup/accounts/sso.component.ts | 99 +- src/popup/accounts/two-factor.component.ts | 220 ++- .../update-temp-password.component.ts | 124 +- src/popup/app.component.ts | 453 ++--- .../components/action-buttons.component.ts | 132 +- .../generator/password-generator.component.ts | 68 +- src/popup/send/send-add-edit.component.ts | 245 +-- src/popup/send/send-groupings.component.ts | 339 ++-- src/popup/send/send-type.component.ts | 280 +-- src/popup/services/services.module.ts | 461 +++-- .../settings/excluded-domains.component.ts | 174 +- src/popup/settings/options.component.ts | 249 +-- src/popup/settings/premium.component.ts | 47 +- src/popup/settings/settings.component.ts | 694 +++---- src/popup/vault/add-edit.component.ts | 347 ++-- src/popup/vault/attachments.component.ts | 85 +- src/popup/vault/ciphers.component.ts | 428 ++--- src/popup/vault/collections.component.ts | 64 +- src/popup/vault/current-tab.component.ts | 430 ++--- src/popup/vault/groupings.component.ts | 656 +++---- src/popup/vault/share.component.ts | 92 +- src/popup/vault/view.component.ts | 508 +++--- src/services/abstractions/state.service.ts | 43 +- src/services/browserCrypto.service.ts | 18 +- src/services/browserPlatformUtils.service.ts | 657 +++---- src/services/browserStorage.service.ts | 80 +- src/services/state.service.ts | 107 +- 40 files changed, 5928 insertions(+), 5299 deletions(-) diff --git a/src/background/idle.background.ts b/src/background/idle.background.ts index 8fb603ea9c..9dee1fc1cc 100644 --- a/src/background/idle.background.ts +++ b/src/background/idle.background.ts @@ -1,66 +1,71 @@ -import { NotificationsService } from 'jslib-common/abstractions/notifications.service'; -import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; -import { StateService } from '../services/abstractions/state.service'; +import { NotificationsService } from "jslib-common/abstractions/notifications.service"; +import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; +import { StateService } from "../services/abstractions/state.service"; const IdleInterval = 60 * 5; // 5 minutes export default class IdleBackground { - private idle: any; - private idleTimer: number = null; - private idleState = 'active'; + private idle: any; + private idleTimer: number = null; + private idleState = "active"; - constructor(private vaultTimeoutService: VaultTimeoutService, private stateService: StateService, - private notificationsService: NotificationsService) { - this.idle = chrome.idle || (browser != null ? browser.idle : null); + constructor( + private vaultTimeoutService: VaultTimeoutService, + private stateService: StateService, + private notificationsService: NotificationsService + ) { + this.idle = chrome.idle || (browser != null ? browser.idle : null); + } + + async init() { + if (!this.idle) { + return; } - async init() { - if (!this.idle) { - return; - } + const idleHandler = (newState: string) => { + if (newState === "active") { + this.notificationsService.reconnectFromActivity(); + } else { + this.notificationsService.disconnectFromInactivity(); + } + }; + if (this.idle.onStateChanged && this.idle.setDetectionInterval) { + this.idle.setDetectionInterval(IdleInterval); + this.idle.onStateChanged.addListener(idleHandler); + } else { + this.pollIdle(idleHandler); + } - const idleHandler = (newState: string) => { - if (newState === 'active') { - this.notificationsService.reconnectFromActivity(); + if (this.idle.onStateChanged) { + this.idle.onStateChanged.addListener(async (newState: string) => { + if (newState === "locked") { + // If the screen is locked or the screensaver activates + const timeout = await this.stateService.getVaultTimeout(); + if (timeout === -2) { + // On System Lock vault timeout option + const action = await this.stateService.getVaultTimeoutAction(); + if (action === "logOut") { + await this.vaultTimeoutService.logOut(); } else { - this.notificationsService.disconnectFromInactivity(); + await this.vaultTimeoutService.lock(true); } - }; - if (this.idle.onStateChanged && this.idle.setDetectionInterval) { - this.idle.setDetectionInterval(IdleInterval); - this.idle.onStateChanged.addListener(idleHandler); - } else { - this.pollIdle(idleHandler); - } - - if (this.idle.onStateChanged) { - this.idle.onStateChanged.addListener(async (newState: string) => { - if (newState === 'locked') { // If the screen is locked or the screensaver activates - const timeout = await this.stateService.getVaultTimeout(); - if (timeout === -2) { // On System Lock vault timeout option - const action = await this.stateService.getVaultTimeoutAction(); - if (action === 'logOut') { - await this.vaultTimeoutService.logOut(); - } else { - await this.vaultTimeoutService.lock(true); - } - } - } - }); + } } + }); } + } - private pollIdle(handler: (newState: string) => void) { - if (this.idleTimer != null) { - window.clearTimeout(this.idleTimer); - this.idleTimer = null; - } - this.idle.queryState(IdleInterval, (state: string) => { - if (state !== this.idleState) { - this.idleState = state; - handler(state); - } - this.idleTimer = window.setTimeout(() => this.pollIdle(handler), 5000); - }); + private pollIdle(handler: (newState: string) => void) { + if (this.idleTimer != null) { + window.clearTimeout(this.idleTimer); + this.idleTimer = null; } + this.idle.queryState(IdleInterval, (state: string) => { + if (state !== this.idleState) { + this.idleState = state; + handler(state); + } + this.idleTimer = window.setTimeout(() => this.pollIdle(handler), 5000); + }); + } } diff --git a/src/background/main.background.ts b/src/background/main.background.ts index 30e48258fa..34553dffae 100644 --- a/src/background/main.background.ts +++ b/src/background/main.background.ts @@ -1,801 +1,966 @@ -import { CipherRepromptType } from 'jslib-common/enums/cipherRepromptType'; -import { CipherType } from 'jslib-common/enums/cipherType'; +import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType"; +import { CipherType } from "jslib-common/enums/cipherType"; -import { CipherView } from 'jslib-common/models/view/cipherView'; +import { CipherView } from "jslib-common/models/view/cipherView"; -import { ApiService } from 'jslib-common/services/api.service'; -import { AppIdService } from 'jslib-common/services/appId.service'; -import { AuditService } from 'jslib-common/services/audit.service'; -import { AuthService } from 'jslib-common/services/auth.service'; -import { CipherService } from 'jslib-common/services/cipher.service'; -import { CollectionService } from 'jslib-common/services/collection.service'; -import { ConsoleLogService } from 'jslib-common/services/consoleLog.service'; -import { ContainerService } from 'jslib-common/services/container.service'; -import { EnvironmentService } from 'jslib-common/services/environment.service'; -import { EventService } from 'jslib-common/services/event.service'; -import { ExportService } from 'jslib-common/services/export.service'; -import { FileUploadService } from 'jslib-common/services/fileUpload.service'; -import { FolderService } from 'jslib-common/services/folder.service'; -import { KeyConnectorService } from 'jslib-common/services/keyConnector.service'; -import { NotificationsService } from 'jslib-common/services/notifications.service'; -import { OrganizationService } from 'jslib-common/services/organization.service'; -import { PasswordGenerationService } from 'jslib-common/services/passwordGeneration.service'; -import { PolicyService } from 'jslib-common/services/policy.service'; -import { ProviderService } from 'jslib-common/services/provider.service'; -import { SearchService } from 'jslib-common/services/search.service'; -import { SendService } from 'jslib-common/services/send.service'; -import { SettingsService } from 'jslib-common/services/settings.service'; -import { StateMigrationService } from 'jslib-common/services/stateMigration.service'; -import { SyncService } from 'jslib-common/services/sync.service'; -import { SystemService } from 'jslib-common/services/system.service'; -import { TokenService } from 'jslib-common/services/token.service'; -import { TotpService } from 'jslib-common/services/totp.service'; -import { WebCryptoFunctionService } from 'jslib-common/services/webCryptoFunction.service'; +import { ApiService } from "jslib-common/services/api.service"; +import { AppIdService } from "jslib-common/services/appId.service"; +import { AuditService } from "jslib-common/services/audit.service"; +import { AuthService } from "jslib-common/services/auth.service"; +import { CipherService } from "jslib-common/services/cipher.service"; +import { CollectionService } from "jslib-common/services/collection.service"; +import { ConsoleLogService } from "jslib-common/services/consoleLog.service"; +import { ContainerService } from "jslib-common/services/container.service"; +import { EnvironmentService } from "jslib-common/services/environment.service"; +import { EventService } from "jslib-common/services/event.service"; +import { ExportService } from "jslib-common/services/export.service"; +import { FileUploadService } from "jslib-common/services/fileUpload.service"; +import { FolderService } from "jslib-common/services/folder.service"; +import { KeyConnectorService } from "jslib-common/services/keyConnector.service"; +import { NotificationsService } from "jslib-common/services/notifications.service"; +import { OrganizationService } from "jslib-common/services/organization.service"; +import { PasswordGenerationService } from "jslib-common/services/passwordGeneration.service"; +import { PolicyService } from "jslib-common/services/policy.service"; +import { ProviderService } from "jslib-common/services/provider.service"; +import { SearchService } from "jslib-common/services/search.service"; +import { SendService } from "jslib-common/services/send.service"; +import { SettingsService } from "jslib-common/services/settings.service"; +import { StateMigrationService } from "jslib-common/services/stateMigration.service"; +import { SyncService } from "jslib-common/services/sync.service"; +import { SystemService } from "jslib-common/services/system.service"; +import { TokenService } from "jslib-common/services/token.service"; +import { TotpService } from "jslib-common/services/totp.service"; +import { WebCryptoFunctionService } from "jslib-common/services/webCryptoFunction.service"; -import { ApiService as ApiServiceAbstraction } from 'jslib-common/abstractions/api.service'; -import { AppIdService as AppIdServiceAbstraction } from 'jslib-common/abstractions/appId.service'; -import { AuditService as AuditServiceAbstraction } from 'jslib-common/abstractions/audit.service'; -import { AuthService as AuthServiceAbstraction } from 'jslib-common/abstractions/auth.service'; -import { CipherService as CipherServiceAbstraction } from 'jslib-common/abstractions/cipher.service'; -import { CollectionService as CollectionServiceAbstraction } from 'jslib-common/abstractions/collection.service'; -import { CryptoService as CryptoServiceAbstraction } from 'jslib-common/abstractions/crypto.service'; -import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from 'jslib-common/abstractions/cryptoFunction.service'; -import { EnvironmentService as EnvironmentServiceAbstraction } from 'jslib-common/abstractions/environment.service'; -import { EventService as EventServiceAbstraction } from 'jslib-common/abstractions/event.service'; -import { ExportService as ExportServiceAbstraction } from 'jslib-common/abstractions/export.service'; -import { FileUploadService as FileUploadServiceAbstraction } from 'jslib-common/abstractions/fileUpload.service'; -import { FolderService as FolderServiceAbstraction } from 'jslib-common/abstractions/folder.service'; -import { I18nService as I18nServiceAbstraction } from 'jslib-common/abstractions/i18n.service'; -import { KeyConnectorService as KeyConnectorServiceAbstraction } from 'jslib-common/abstractions/keyConnector.service'; -import { LogService as LogServiceAbstraction } from 'jslib-common/abstractions/log.service'; -import { MessagingService as MessagingServiceAbstraction } from 'jslib-common/abstractions/messaging.service'; -import { NotificationsService as NotificationsServiceAbstraction } from 'jslib-common/abstractions/notifications.service'; -import { OrganizationService as OrganizationServiceAbstraction } from 'jslib-common/abstractions/organization.service'; -import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from 'jslib-common/abstractions/passwordGeneration.service'; -import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService as PolicyServiceAbstraction } from 'jslib-common/abstractions/policy.service'; -import { ProviderService as ProviderServiceAbstraction } from 'jslib-common/abstractions/provider.service'; -import { SearchService as SearchServiceAbstraction } from 'jslib-common/abstractions/search.service'; -import { SendService as SendServiceAbstraction } from 'jslib-common/abstractions/send.service'; -import { SettingsService as SettingsServiceAbstraction } from 'jslib-common/abstractions/settings.service'; -import { StorageService as StorageServiceAbstraction } from 'jslib-common/abstractions/storage.service'; -import { SyncService as SyncServiceAbstraction } from 'jslib-common/abstractions/sync.service'; -import { SystemService as SystemServiceAbstraction } from 'jslib-common/abstractions/system.service'; -import { TokenService as TokenServiceAbstraction } from 'jslib-common/abstractions/token.service'; -import { TotpService as TotpServiceAbstraction } from 'jslib-common/abstractions/totp.service'; -import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from 'jslib-common/abstractions/vaultTimeout.service'; -import { AutofillService as AutofillServiceAbstraction } from '../services/abstractions/autofill.service'; +import { ApiService as ApiServiceAbstraction } from "jslib-common/abstractions/api.service"; +import { AppIdService as AppIdServiceAbstraction } from "jslib-common/abstractions/appId.service"; +import { AuditService as AuditServiceAbstraction } from "jslib-common/abstractions/audit.service"; +import { AuthService as AuthServiceAbstraction } from "jslib-common/abstractions/auth.service"; +import { CipherService as CipherServiceAbstraction } from "jslib-common/abstractions/cipher.service"; +import { CollectionService as CollectionServiceAbstraction } from "jslib-common/abstractions/collection.service"; +import { CryptoService as CryptoServiceAbstraction } from "jslib-common/abstractions/crypto.service"; +import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "jslib-common/abstractions/cryptoFunction.service"; +import { EnvironmentService as EnvironmentServiceAbstraction } from "jslib-common/abstractions/environment.service"; +import { EventService as EventServiceAbstraction } from "jslib-common/abstractions/event.service"; +import { ExportService as ExportServiceAbstraction } from "jslib-common/abstractions/export.service"; +import { FileUploadService as FileUploadServiceAbstraction } from "jslib-common/abstractions/fileUpload.service"; +import { FolderService as FolderServiceAbstraction } from "jslib-common/abstractions/folder.service"; +import { I18nService as I18nServiceAbstraction } from "jslib-common/abstractions/i18n.service"; +import { KeyConnectorService as KeyConnectorServiceAbstraction } from "jslib-common/abstractions/keyConnector.service"; +import { LogService as LogServiceAbstraction } from "jslib-common/abstractions/log.service"; +import { MessagingService as MessagingServiceAbstraction } from "jslib-common/abstractions/messaging.service"; +import { NotificationsService as NotificationsServiceAbstraction } from "jslib-common/abstractions/notifications.service"; +import { OrganizationService as OrganizationServiceAbstraction } from "jslib-common/abstractions/organization.service"; +import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "jslib-common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService as PolicyServiceAbstraction } from "jslib-common/abstractions/policy.service"; +import { ProviderService as ProviderServiceAbstraction } from "jslib-common/abstractions/provider.service"; +import { SearchService as SearchServiceAbstraction } from "jslib-common/abstractions/search.service"; +import { SendService as SendServiceAbstraction } from "jslib-common/abstractions/send.service"; +import { SettingsService as SettingsServiceAbstraction } from "jslib-common/abstractions/settings.service"; +import { StorageService as StorageServiceAbstraction } from "jslib-common/abstractions/storage.service"; +import { SyncService as SyncServiceAbstraction } from "jslib-common/abstractions/sync.service"; +import { SystemService as SystemServiceAbstraction } from "jslib-common/abstractions/system.service"; +import { TokenService as TokenServiceAbstraction } from "jslib-common/abstractions/token.service"; +import { TotpService as TotpServiceAbstraction } from "jslib-common/abstractions/totp.service"; +import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "jslib-common/abstractions/vaultTimeout.service"; +import { AutofillService as AutofillServiceAbstraction } from "../services/abstractions/autofill.service"; -import { BrowserApi } from '../browser/browserApi'; -import { SafariApp } from '../browser/safariApp'; +import { BrowserApi } from "../browser/browserApi"; +import { SafariApp } from "../browser/safariApp"; -import CommandsBackground from './commands.background'; -import ContextMenusBackground from './contextMenus.background'; -import IdleBackground from './idle.background'; -import { NativeMessagingBackground } from './nativeMessaging.background'; -import NotificationBackground from './notification.background'; -import RuntimeBackground from './runtime.background'; -import TabsBackground from './tabs.background'; -import WebRequestBackground from './webRequest.background'; -import WindowsBackground from './windows.background'; +import CommandsBackground from "./commands.background"; +import ContextMenusBackground from "./contextMenus.background"; +import IdleBackground from "./idle.background"; +import { NativeMessagingBackground } from "./nativeMessaging.background"; +import NotificationBackground from "./notification.background"; +import RuntimeBackground from "./runtime.background"; +import TabsBackground from "./tabs.background"; +import WebRequestBackground from "./webRequest.background"; +import WindowsBackground from "./windows.background"; -import { StateService as StateServiceAbstraction } from '../services/abstractions/state.service'; +import { StateService as StateServiceAbstraction } from "../services/abstractions/state.service"; -import { Utils } from 'jslib-common/misc/utils'; -import { PopupUtilsService } from '../popup/services/popup-utils.service'; -import AutofillService from '../services/autofill.service'; -import { BrowserCryptoService } from '../services/browserCrypto.service'; -import BrowserMessagingService from '../services/browserMessaging.service'; -import BrowserPlatformUtilsService from '../services/browserPlatformUtils.service'; -import BrowserStorageService from '../services/browserStorage.service'; -import I18nService from '../services/i18n.service'; -import { StateService } from '../services/state.service'; -import VaultTimeoutService from '../services/vaultTimeout.service'; +import { Utils } from "jslib-common/misc/utils"; +import { PopupUtilsService } from "../popup/services/popup-utils.service"; +import AutofillService from "../services/autofill.service"; +import { BrowserCryptoService } from "../services/browserCrypto.service"; +import BrowserMessagingService from "../services/browserMessaging.service"; +import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service"; +import BrowserStorageService from "../services/browserStorage.service"; +import I18nService from "../services/i18n.service"; +import { StateService } from "../services/state.service"; +import VaultTimeoutService from "../services/vaultTimeout.service"; export default class MainBackground { - messagingService: MessagingServiceAbstraction; - storageService: StorageServiceAbstraction; - secureStorageService: StorageServiceAbstraction; - i18nService: I18nServiceAbstraction; - platformUtilsService: PlatformUtilsServiceAbstraction; - logService: LogServiceAbstraction; - cryptoService: CryptoServiceAbstraction; - cryptoFunctionService: CryptoFunctionServiceAbstraction; - tokenService: TokenServiceAbstraction; - appIdService: AppIdServiceAbstraction; - apiService: ApiServiceAbstraction; - environmentService: EnvironmentServiceAbstraction; - settingsService: SettingsServiceAbstraction; - cipherService: CipherServiceAbstraction; - folderService: FolderServiceAbstraction; - collectionService: CollectionServiceAbstraction; - vaultTimeoutService: VaultTimeoutServiceAbstraction; - syncService: SyncServiceAbstraction; - passwordGenerationService: PasswordGenerationServiceAbstraction; - totpService: TotpServiceAbstraction; - autofillService: AutofillServiceAbstraction; - containerService: ContainerService; - auditService: AuditServiceAbstraction; - authService: AuthServiceAbstraction; - exportService: ExportServiceAbstraction; - searchService: SearchServiceAbstraction; - notificationsService: NotificationsServiceAbstraction; - stateService: StateServiceAbstraction; - stateMigrationService: StateMigrationService; - systemService: SystemServiceAbstraction; - eventService: EventServiceAbstraction; - policyService: PolicyServiceAbstraction; - popupUtilsService: PopupUtilsService; - sendService: SendServiceAbstraction; - fileUploadService: FileUploadServiceAbstraction; - organizationService: OrganizationServiceAbstraction; - providerService: ProviderServiceAbstraction; - keyConnectorService: KeyConnectorServiceAbstraction; + messagingService: MessagingServiceAbstraction; + storageService: StorageServiceAbstraction; + secureStorageService: StorageServiceAbstraction; + i18nService: I18nServiceAbstraction; + platformUtilsService: PlatformUtilsServiceAbstraction; + logService: LogServiceAbstraction; + cryptoService: CryptoServiceAbstraction; + cryptoFunctionService: CryptoFunctionServiceAbstraction; + tokenService: TokenServiceAbstraction; + appIdService: AppIdServiceAbstraction; + apiService: ApiServiceAbstraction; + environmentService: EnvironmentServiceAbstraction; + settingsService: SettingsServiceAbstraction; + cipherService: CipherServiceAbstraction; + folderService: FolderServiceAbstraction; + collectionService: CollectionServiceAbstraction; + vaultTimeoutService: VaultTimeoutServiceAbstraction; + syncService: SyncServiceAbstraction; + passwordGenerationService: PasswordGenerationServiceAbstraction; + totpService: TotpServiceAbstraction; + autofillService: AutofillServiceAbstraction; + containerService: ContainerService; + auditService: AuditServiceAbstraction; + authService: AuthServiceAbstraction; + exportService: ExportServiceAbstraction; + searchService: SearchServiceAbstraction; + notificationsService: NotificationsServiceAbstraction; + stateService: StateServiceAbstraction; + stateMigrationService: StateMigrationService; + systemService: SystemServiceAbstraction; + eventService: EventServiceAbstraction; + policyService: PolicyServiceAbstraction; + popupUtilsService: PopupUtilsService; + sendService: SendServiceAbstraction; + fileUploadService: FileUploadServiceAbstraction; + organizationService: OrganizationServiceAbstraction; + providerService: ProviderServiceAbstraction; + keyConnectorService: KeyConnectorServiceAbstraction; - onUpdatedRan: boolean; - onReplacedRan: boolean; - loginToAutoFill: CipherView = null; - notificationQueue: any[] = []; + onUpdatedRan: boolean; + onReplacedRan: boolean; + loginToAutoFill: CipherView = null; + notificationQueue: any[] = []; - private commandsBackground: CommandsBackground; - private contextMenusBackground: ContextMenusBackground; - private idleBackground: IdleBackground; - private notificationBackground: NotificationBackground; - private runtimeBackground: RuntimeBackground; - private tabsBackground: TabsBackground; - private webRequestBackground: WebRequestBackground; - private windowsBackground: WindowsBackground; + private commandsBackground: CommandsBackground; + private contextMenusBackground: ContextMenusBackground; + private idleBackground: IdleBackground; + private notificationBackground: NotificationBackground; + private runtimeBackground: RuntimeBackground; + private tabsBackground: TabsBackground; + private webRequestBackground: WebRequestBackground; + private windowsBackground: WindowsBackground; - private sidebarAction: any; - private buildingContextMenu: boolean; - private menuOptionsLoaded: any[] = []; - private syncTimeout: any; - private isSafari: boolean; - private nativeMessagingBackground: NativeMessagingBackground; + private sidebarAction: any; + private buildingContextMenu: boolean; + private menuOptionsLoaded: any[] = []; + private syncTimeout: any; + private isSafari: boolean; + private nativeMessagingBackground: NativeMessagingBackground; - constructor() { - // Services - this.messagingService = new BrowserMessagingService(); - this.storageService = new BrowserStorageService(); - this.secureStorageService = new BrowserStorageService(); - this.logService = new ConsoleLogService(false); - this.stateMigrationService = new StateMigrationService(this.storageService, this.secureStorageService); - this.stateService = new StateService(this.storageService, this.secureStorageService, this.logService, this.stateMigrationService); - this.platformUtilsService = new BrowserPlatformUtilsService(this.messagingService, this.stateService, - (clipboardValue, clearMs) => { - if (this.systemService != null) { - this.systemService.clearClipboard(clipboardValue, clearMs); - } - }, - async () => { - if (this.nativeMessagingBackground != null) { - const promise = this.nativeMessagingBackground.getResponse(); - - try { - await this.nativeMessagingBackground.send({ command: 'biometricUnlock' }); - } catch (e) { - return Promise.reject(e); - } - - return promise.then(result => result.response === 'unlocked'); - } - }); - this.i18nService = new I18nService(BrowserApi.getUILanguage(window)); - this.cryptoFunctionService = new WebCryptoFunctionService(window, this.platformUtilsService); - this.cryptoService = new BrowserCryptoService(this.cryptoFunctionService, this.platformUtilsService, - this.logService, this.stateService); - this.tokenService = new TokenService(this.stateService); - this.appIdService = new AppIdService(this.storageService); - this.environmentService = new EnvironmentService(this.stateService); - this.apiService = new ApiService(this.tokenService, this.platformUtilsService, this.environmentService, - (expired: boolean) => this.logout(expired)); - this.settingsService = new SettingsService(this.stateService); - this.fileUploadService = new FileUploadService(this.logService, this.apiService); - this.cipherService = new CipherService(this.cryptoService, this.settingsService, - this.apiService, this.fileUploadService, - this.i18nService, () => this.searchService, - this.logService, this.stateService); - this.folderService = new FolderService(this.cryptoService, this.apiService, - this.i18nService, this.cipherService, - this.stateService); - this.collectionService = new CollectionService(this.cryptoService, this.i18nService, - this.stateService); - this.searchService = new SearchService(this.cipherService, this.logService, this.i18nService); - this.sendService = new SendService(this.cryptoService, this.apiService, - this.fileUploadService, this.i18nService, - this.cryptoFunctionService, this.stateService); - this.organizationService = new OrganizationService(this.stateService); - this.policyService = new PolicyService(this.stateService, this.organizationService, this.apiService); - this.keyConnectorService = new KeyConnectorService(this.stateService, this.cryptoService, - this.apiService, this.tokenService, this.logService, this.organizationService); - - const vaultTimeoutServiceCallbacks = { - locked: async () => { - if (this.notificationsService != null) { - this.notificationsService.updateConnection(false); - } - await this.setIcon(); - await this.refreshBadgeAndMenu(true); - if (this.systemService != null) { - this.systemService.startProcessReload(); - await this.systemService.clearPendingClipboard(); - } - }, - logout: async () => await this.logout(false), - }; - this.vaultTimeoutService = new VaultTimeoutService(this.cipherService, - this.folderService, - this.collectionService, - this.cryptoService, - this.platformUtilsService, - this.messagingService, - this.searchService, - this.tokenService, - this.policyService, - this.keyConnectorService, - this.stateService, vaultTimeoutServiceCallbacks.locked, vaultTimeoutServiceCallbacks.logout); - - this.providerService = new ProviderService(this.stateService); - this.syncService = new SyncService(this.apiService, this.settingsService, - this.folderService, this.cipherService, - this.cryptoService, this.collectionService, - this.messagingService, this.policyService, - this.sendService, this.logService, this.keyConnectorService, - this.stateService, this.organizationService, - this.providerService, async (expired: boolean) => await this.logout(expired)); - this.eventService = new EventService(this.apiService, this.cipherService, - this.stateService, this.logService, this.organizationService); - this.passwordGenerationService = new PasswordGenerationService(this.cryptoService, this.policyService, - this.stateService); - this.totpService = new TotpService(this.cryptoFunctionService, this.logService, this.stateService); - this.autofillService = new AutofillService(this.cipherService, this.stateService, - this.totpService, this.eventService, this.logService); - this.containerService = new ContainerService(this.cryptoService); - this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); - this.exportService = new ExportService(this.folderService, this.cipherService, this.apiService, - this.cryptoService); - this.notificationsService = new NotificationsService(this.syncService, this.appIdService, - this.apiService, this.vaultTimeoutService, - this.environmentService, () => this.logout(true), - this.logService, this.stateService); - this.popupUtilsService = new PopupUtilsService(this.platformUtilsService); - - const systemUtilsServiceReloadCallback = () => { - const forceWindowReload = this.platformUtilsService.isSafari() || - this.platformUtilsService.isFirefox() || this.platformUtilsService.isOpera(); - BrowserApi.reloadExtension(forceWindowReload ? window : null); - return Promise.resolve(); - }; - this.systemService = new SystemService( - this.messagingService, this.platformUtilsService, systemUtilsServiceReloadCallback, this.stateService); - - // Other fields - this.isSafari = this.platformUtilsService.isSafari(); - this.sidebarAction = this.isSafari ? null : (typeof opr !== 'undefined') && opr.sidebarAction ? - opr.sidebarAction : (window as any).chrome.sidebarAction; - - // Background - this.runtimeBackground = new RuntimeBackground(this, this.autofillService, - this.platformUtilsService as BrowserPlatformUtilsService, - this.i18nService, this.notificationsService, - this.systemService, this.environmentService, - this.messagingService, this.stateService, this.logService); - this.nativeMessagingBackground = new NativeMessagingBackground(this.cryptoService, - this.cryptoFunctionService, this.runtimeBackground, - this.i18nService, this.messagingService, this.appIdService, - this.platformUtilsService, this.stateService); - this.commandsBackground = new CommandsBackground(this, this.passwordGenerationService, - this.platformUtilsService, this.vaultTimeoutService); - this.notificationBackground = new NotificationBackground(this, this.autofillService, this.cipherService, - this.storageService, this.vaultTimeoutService, this.policyService, this.folderService, this.stateService); - - this.tabsBackground = new TabsBackground(this, this.notificationBackground); - this.contextMenusBackground = new ContextMenusBackground(this, this.cipherService, this.passwordGenerationService, - this.platformUtilsService, this.vaultTimeoutService, this.eventService, this.totpService); - this.idleBackground = new IdleBackground(this.vaultTimeoutService, this.stateService, - this.notificationsService); - this.webRequestBackground = new WebRequestBackground(this.platformUtilsService, this.cipherService, - this.vaultTimeoutService); - this.windowsBackground = new WindowsBackground(this); - - const that = this; - const backgroundMessagingService = new class extends MessagingServiceAbstraction { - // AuthService should send the messages to the background not popup. - send = (subscriber: string, arg: any = {}) => { - const message = Object.assign({}, { command: subscriber }, arg); - that.runtimeBackground.processMessage(message, that, null); - } - }(); - - this.authService = new AuthService(this.cryptoService, this.apiService, - this.tokenService, this.appIdService, - this.i18nService, this.platformUtilsService, - backgroundMessagingService, this.vaultTimeoutService, - this.logService, this.cryptoFunctionService, this.keyConnectorService, this.environmentService, this.stateService); - } - - async bootstrap() { - this.containerService.attachToWindow(window); - - (this.authService as AuthService).init(); - await (this.vaultTimeoutService as VaultTimeoutService).init(true); - await (this.i18nService as I18nService).init(); - await (this.eventService as EventService).init(true); - await this.runtimeBackground.init(); - await this.notificationBackground.init(); - await this.commandsBackground.init(); - await this.stateService.init(); - - await this.tabsBackground.init(); - await this.contextMenusBackground.init(); - await this.idleBackground.init(); - await this.webRequestBackground.init(); - await this.windowsBackground.init(); - - return new Promise(resolve => { - setTimeout(async () => { - await this.environmentService.setUrlsFromStorage(); - await this.setIcon(); - this.fullSync(true); - setTimeout(() => this.notificationsService.init(), 2500); - resolve(); - }, 500); - }); - } - - async setIcon() { - if (!chrome.browserAction && !this.sidebarAction) { - return; + constructor() { + // Services + this.messagingService = new BrowserMessagingService(); + this.storageService = new BrowserStorageService(); + this.secureStorageService = new BrowserStorageService(); + this.logService = new ConsoleLogService(false); + this.stateMigrationService = new StateMigrationService( + this.storageService, + this.secureStorageService + ); + this.stateService = new StateService( + this.storageService, + this.secureStorageService, + this.logService, + this.stateMigrationService + ); + this.platformUtilsService = new BrowserPlatformUtilsService( + this.messagingService, + this.stateService, + (clipboardValue, clearMs) => { + if (this.systemService != null) { + this.systemService.clearClipboard(clipboardValue, clearMs); } + }, + async () => { + if (this.nativeMessagingBackground != null) { + const promise = this.nativeMessagingBackground.getResponse(); - const isAuthenticated = await this.stateService.getIsAuthenticated(); - const locked = await this.vaultTimeoutService.isLocked(); + try { + await this.nativeMessagingBackground.send({ command: "biometricUnlock" }); + } catch (e) { + return Promise.reject(e); + } - let suffix = ''; - if (!isAuthenticated) { - suffix = '_gray'; - } else if (locked) { - suffix = '_locked'; + return promise.then((result) => result.response === "unlocked"); } + } + ); + this.i18nService = new I18nService(BrowserApi.getUILanguage(window)); + this.cryptoFunctionService = new WebCryptoFunctionService(window, this.platformUtilsService); + this.cryptoService = new BrowserCryptoService( + this.cryptoFunctionService, + this.platformUtilsService, + this.logService, + this.stateService + ); + this.tokenService = new TokenService(this.stateService); + this.appIdService = new AppIdService(this.storageService); + this.environmentService = new EnvironmentService(this.stateService); + this.apiService = new ApiService( + this.tokenService, + this.platformUtilsService, + this.environmentService, + (expired: boolean) => this.logout(expired) + ); + this.settingsService = new SettingsService(this.stateService); + this.fileUploadService = new FileUploadService(this.logService, this.apiService); + this.cipherService = new CipherService( + this.cryptoService, + this.settingsService, + this.apiService, + this.fileUploadService, + this.i18nService, + () => this.searchService, + this.logService, + this.stateService + ); + this.folderService = new FolderService( + this.cryptoService, + this.apiService, + this.i18nService, + this.cipherService, + this.stateService + ); + this.collectionService = new CollectionService( + this.cryptoService, + this.i18nService, + this.stateService + ); + this.searchService = new SearchService(this.cipherService, this.logService, this.i18nService); + this.sendService = new SendService( + this.cryptoService, + this.apiService, + this.fileUploadService, + this.i18nService, + this.cryptoFunctionService, + this.stateService + ); + this.organizationService = new OrganizationService(this.stateService); + this.policyService = new PolicyService( + this.stateService, + this.organizationService, + this.apiService + ); + this.keyConnectorService = new KeyConnectorService( + this.stateService, + this.cryptoService, + this.apiService, + this.tokenService, + this.logService, + this.organizationService + ); - await this.actionSetIcon(chrome.browserAction, suffix); - await this.actionSetIcon(this.sidebarAction, suffix); - } - - async refreshBadgeAndMenu(forLocked: boolean = false) { - if (!chrome.windows || !chrome.contextMenus) { - return; + const vaultTimeoutServiceCallbacks = { + locked: async () => { + if (this.notificationsService != null) { + this.notificationsService.updateConnection(false); } - - const menuDisabled = await this.stateService.getDisableContextMenuItem(); - if (!menuDisabled) { - await this.buildContextMenu(); - } else { - await this.contextMenusRemoveAll(); - } - - if (forLocked) { - await this.loadMenuAndUpdateBadgeForNoAccessState(!menuDisabled); - this.onUpdatedRan = this.onReplacedRan = false; - return; - } - - const tab = await BrowserApi.getTabFromCurrentWindow(); - if (tab) { - await this.contextMenuReady(tab, !menuDisabled); - } - } - - async logout(expired: boolean) { - await this.eventService.uploadEvents(); - const userId = await this.stateService.getUserId(); - - await Promise.all([ - this.eventService.clearEvents(), - this.syncService.setLastSync(new Date(0)), - this.tokenService.clearToken(), - this.cryptoService.clearKeys(), - this.settingsService.clear(userId), - this.cipherService.clear(userId), - this.folderService.clear(userId), - this.collectionService.clear(userId), - this.policyService.clear(userId), - this.passwordGenerationService.clear(), - this.vaultTimeoutService.clear(), - this.keyConnectorService.clear(), - ]); - - this.searchService.clearIndex(); - this.messagingService.send('doneLoggingOut', { expired: expired }); - await this.setIcon(); - await this.refreshBadgeAndMenu(); - await this.reseedStorage(); - this.notificationsService.updateConnection(false); - this.systemService.startProcessReload(); - await this.systemService.clearPendingClipboard(); + await this.refreshBadgeAndMenu(true); + if (this.systemService != null) { + this.systemService.startProcessReload(); + await this.systemService.clearPendingClipboard(); + } + }, + logout: async () => await this.logout(false), + }; + this.vaultTimeoutService = new VaultTimeoutService( + this.cipherService, + this.folderService, + this.collectionService, + this.cryptoService, + this.platformUtilsService, + this.messagingService, + this.searchService, + this.tokenService, + this.policyService, + this.keyConnectorService, + this.stateService, + vaultTimeoutServiceCallbacks.locked, + vaultTimeoutServiceCallbacks.logout + ); + + this.providerService = new ProviderService(this.stateService); + this.syncService = new SyncService( + this.apiService, + this.settingsService, + this.folderService, + this.cipherService, + this.cryptoService, + this.collectionService, + this.messagingService, + this.policyService, + this.sendService, + this.logService, + this.keyConnectorService, + this.stateService, + this.organizationService, + this.providerService, + async (expired: boolean) => await this.logout(expired) + ); + this.eventService = new EventService( + this.apiService, + this.cipherService, + this.stateService, + this.logService, + this.organizationService + ); + this.passwordGenerationService = new PasswordGenerationService( + this.cryptoService, + this.policyService, + this.stateService + ); + this.totpService = new TotpService( + this.cryptoFunctionService, + this.logService, + this.stateService + ); + this.autofillService = new AutofillService( + this.cipherService, + this.stateService, + this.totpService, + this.eventService, + this.logService + ); + this.containerService = new ContainerService(this.cryptoService); + this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); + this.exportService = new ExportService( + this.folderService, + this.cipherService, + this.apiService, + this.cryptoService + ); + this.notificationsService = new NotificationsService( + this.syncService, + this.appIdService, + this.apiService, + this.vaultTimeoutService, + this.environmentService, + () => this.logout(true), + this.logService, + this.stateService + ); + this.popupUtilsService = new PopupUtilsService(this.platformUtilsService); + + const systemUtilsServiceReloadCallback = () => { + const forceWindowReload = + this.platformUtilsService.isSafari() || + this.platformUtilsService.isFirefox() || + this.platformUtilsService.isOpera(); + BrowserApi.reloadExtension(forceWindowReload ? window : null); + return Promise.resolve(); + }; + this.systemService = new SystemService( + this.messagingService, + this.platformUtilsService, + systemUtilsServiceReloadCallback, + this.stateService + ); + + // Other fields + this.isSafari = this.platformUtilsService.isSafari(); + this.sidebarAction = this.isSafari + ? null + : typeof opr !== "undefined" && opr.sidebarAction + ? opr.sidebarAction + : (window as any).chrome.sidebarAction; + + // Background + this.runtimeBackground = new RuntimeBackground( + this, + this.autofillService, + this.platformUtilsService as BrowserPlatformUtilsService, + this.i18nService, + this.notificationsService, + this.systemService, + this.environmentService, + this.messagingService, + this.stateService, + this.logService + ); + this.nativeMessagingBackground = new NativeMessagingBackground( + this.cryptoService, + this.cryptoFunctionService, + this.runtimeBackground, + this.i18nService, + this.messagingService, + this.appIdService, + this.platformUtilsService, + this.stateService + ); + this.commandsBackground = new CommandsBackground( + this, + this.passwordGenerationService, + this.platformUtilsService, + this.vaultTimeoutService + ); + this.notificationBackground = new NotificationBackground( + this, + this.autofillService, + this.cipherService, + this.storageService, + this.vaultTimeoutService, + this.policyService, + this.folderService, + this.stateService + ); + + this.tabsBackground = new TabsBackground(this, this.notificationBackground); + this.contextMenusBackground = new ContextMenusBackground( + this, + this.cipherService, + this.passwordGenerationService, + this.platformUtilsService, + this.vaultTimeoutService, + this.eventService, + this.totpService + ); + this.idleBackground = new IdleBackground( + this.vaultTimeoutService, + this.stateService, + this.notificationsService + ); + this.webRequestBackground = new WebRequestBackground( + this.platformUtilsService, + this.cipherService, + this.vaultTimeoutService + ); + this.windowsBackground = new WindowsBackground(this); + + const that = this; + const backgroundMessagingService = new (class extends MessagingServiceAbstraction { + // AuthService should send the messages to the background not popup. + send = (subscriber: string, arg: any = {}) => { + const message = Object.assign({}, { command: subscriber }, arg); + that.runtimeBackground.processMessage(message, that, null); + }; + })(); + + this.authService = new AuthService( + this.cryptoService, + this.apiService, + this.tokenService, + this.appIdService, + this.i18nService, + this.platformUtilsService, + backgroundMessagingService, + this.vaultTimeoutService, + this.logService, + this.cryptoFunctionService, + this.keyConnectorService, + this.environmentService, + this.stateService + ); + } + + async bootstrap() { + this.containerService.attachToWindow(window); + + (this.authService as AuthService).init(); + await (this.vaultTimeoutService as VaultTimeoutService).init(true); + await (this.i18nService as I18nService).init(); + await (this.eventService as EventService).init(true); + await this.runtimeBackground.init(); + await this.notificationBackground.init(); + await this.commandsBackground.init(); + await this.stateService.init(); + + await this.tabsBackground.init(); + await this.contextMenusBackground.init(); + await this.idleBackground.init(); + await this.webRequestBackground.init(); + await this.windowsBackground.init(); + + return new Promise((resolve) => { + setTimeout(async () => { + await this.environmentService.setUrlsFromStorage(); + await this.setIcon(); + this.fullSync(true); + setTimeout(() => this.notificationsService.init(), 2500); + resolve(); + }, 500); + }); + } + + async setIcon() { + if (!chrome.browserAction && !this.sidebarAction) { + return; } - async collectPageDetailsForContentScript(tab: any, sender: string, frameId: number = null) { - if (tab == null || !tab.id) { - return; - } + const isAuthenticated = await this.stateService.getIsAuthenticated(); + const locked = await this.vaultTimeoutService.isLocked(); - const options: any = {}; - if (frameId != null) { - options.frameId = frameId; - } - - BrowserApi.tabSendMessage(tab, { - command: 'collectPageDetails', - tab: tab, - sender: sender, - }, options); + let suffix = ""; + if (!isAuthenticated) { + suffix = "_gray"; + } else if (locked) { + suffix = "_locked"; } - async openPopup() { - // Chrome APIs cannot open popup + await this.actionSetIcon(chrome.browserAction, suffix); + await this.actionSetIcon(this.sidebarAction, suffix); + } - // TODO: Do we need to open this popup? - if (!this.isSafari) { - return; - } - await SafariApp.sendMessageToApp('showPopover', null, true); + async refreshBadgeAndMenu(forLocked: boolean = false) { + if (!chrome.windows || !chrome.contextMenus) { + return; } - async reseedStorage() { - if (!this.platformUtilsService.isChrome() && !this.platformUtilsService.isVivaldi() && - !this.platformUtilsService.isOpera()) { - return; - } - - const currentVaultTimeout = await this.stateService.getVaultTimeout(); - if (currentVaultTimeout == null) { - return; - } - - const getStorage = (): Promise => new Promise(resolve => { - chrome.storage.local.get(null, (o: any) => resolve(o)); - }); - - const clearStorage = (): Promise => new Promise(resolve => { - chrome.storage.local.clear(() => resolve()); - }); - - const storage = await getStorage(); - await clearStorage(); - - for (const key in storage) { - if (!storage.hasOwnProperty(key)) { - continue; - } - await this.storageService.save(key, storage[key]); - } + const menuDisabled = await this.stateService.getDisableContextMenuItem(); + if (!menuDisabled) { + await this.buildContextMenu(); + } else { + await this.contextMenusRemoveAll(); } - private async buildContextMenu() { - if (!chrome.contextMenus || this.buildingContextMenu) { - return; - } - - this.buildingContextMenu = true; - await this.contextMenusRemoveAll(); - - await this.contextMenusCreate({ - type: 'normal', - id: 'root', - contexts: ['all'], - title: 'Bitwarden', - }); - - await this.contextMenusCreate({ - type: 'normal', - id: 'autofill', - parentId: 'root', - contexts: ['all'], - title: this.i18nService.t('autoFill'), - }); - - await this.contextMenusCreate({ - type: 'normal', - id: 'copy-username', - parentId: 'root', - contexts: ['all'], - title: this.i18nService.t('copyUsername'), - }); - - await this.contextMenusCreate({ - type: 'normal', - id: 'copy-password', - parentId: 'root', - contexts: ['all'], - title: this.i18nService.t('copyPassword'), - }); - - if (await this.stateService.getCanAccessPremium()) { - await this.contextMenusCreate({ - type: 'normal', - id: 'copy-totp', - parentId: 'root', - contexts: ['all'], - title: this.i18nService.t('copyVerificationCode'), - }); - } - - await this.contextMenusCreate({ - type: 'separator', - parentId: 'root', - }); - - await this.contextMenusCreate({ - type: 'normal', - id: 'generate-password', - parentId: 'root', - contexts: ['all'], - title: this.i18nService.t('generatePasswordCopied'), - }); - - await this.contextMenusCreate({ - type: 'normal', - id: 'copy-identifier', - parentId: 'root', - contexts: ['all'], - title: this.i18nService.t('copyElementIdentifier'), - }); - - this.buildingContextMenu = false; + if (forLocked) { + await this.loadMenuAndUpdateBadgeForNoAccessState(!menuDisabled); + this.onUpdatedRan = this.onReplacedRan = false; + return; } - private async contextMenuReady(tab: any, contextMenuEnabled: boolean) { - await this.loadMenuAndUpdateBadge(tab.url, tab.id, contextMenuEnabled); - this.onUpdatedRan = this.onReplacedRan = false; + const tab = await BrowserApi.getTabFromCurrentWindow(); + if (tab) { + await this.contextMenuReady(tab, !menuDisabled); + } + } + + async logout(expired: boolean) { + await this.eventService.uploadEvents(); + const userId = await this.stateService.getUserId(); + + await Promise.all([ + this.eventService.clearEvents(), + this.syncService.setLastSync(new Date(0)), + this.tokenService.clearToken(), + this.cryptoService.clearKeys(), + this.settingsService.clear(userId), + this.cipherService.clear(userId), + this.folderService.clear(userId), + this.collectionService.clear(userId), + this.policyService.clear(userId), + this.passwordGenerationService.clear(), + this.vaultTimeoutService.clear(), + this.keyConnectorService.clear(), + ]); + + this.searchService.clearIndex(); + this.messagingService.send("doneLoggingOut", { expired: expired }); + + await this.setIcon(); + await this.refreshBadgeAndMenu(); + await this.reseedStorage(); + this.notificationsService.updateConnection(false); + this.systemService.startProcessReload(); + await this.systemService.clearPendingClipboard(); + } + + async collectPageDetailsForContentScript(tab: any, sender: string, frameId: number = null) { + if (tab == null || !tab.id) { + return; } - private async loadMenuAndUpdateBadge(url: string, tabId: number, contextMenuEnabled: boolean) { - if (!url || (!chrome.browserAction && !this.sidebarAction)) { - return; - } - - this.actionSetBadgeBackgroundColor(chrome.browserAction); - this.actionSetBadgeBackgroundColor(this.sidebarAction); - - this.menuOptionsLoaded = []; - const locked = await this.vaultTimeoutService.isLocked(); - if (!locked) { - try { - const ciphers = await this.cipherService.getAllDecryptedForUrl(url); - ciphers.sort((a: any, b: any) => this.cipherService.sortCiphersByLastUsedThenName(a, b)); - - if (contextMenuEnabled) { - ciphers.forEach((cipher: any) => { - this.loadLoginContextMenuOptions(cipher); - }); - } - - const disableBadgeCounter = await this.stateService.getDisableBadgeCounter(); - let theText = ''; - - if (!disableBadgeCounter) { - if (ciphers.length > 0 && ciphers.length <= 9) { - theText = ciphers.length.toString(); - } else if (ciphers.length > 0) { - theText = '9+'; - } - } - - if (contextMenuEnabled && ciphers.length === 0) { - await this.loadNoLoginsContextMenuOptions(this.i18nService.t('noMatchingLogins')); - } - - this.sidebarActionSetBadgeText(theText, tabId); - this.browserActionSetBadgeText(theText, tabId); - - return; - } catch (e) { - this.logService.error(e); - } - } - - await this.loadMenuAndUpdateBadgeForNoAccessState(contextMenuEnabled); + const options: any = {}; + if (frameId != null) { + options.frameId = frameId; } - private async loadMenuAndUpdateBadgeForNoAccessState(contextMenuEnabled: boolean) { + BrowserApi.tabSendMessage( + tab, + { + command: "collectPageDetails", + tab: tab, + sender: sender, + }, + options + ); + } + + async openPopup() { + // Chrome APIs cannot open popup + + // TODO: Do we need to open this popup? + if (!this.isSafari) { + return; + } + await SafariApp.sendMessageToApp("showPopover", null, true); + } + + async reseedStorage() { + if ( + !this.platformUtilsService.isChrome() && + !this.platformUtilsService.isVivaldi() && + !this.platformUtilsService.isOpera() + ) { + return; + } + + const currentVaultTimeout = await this.stateService.getVaultTimeout(); + if (currentVaultTimeout == null) { + return; + } + + const getStorage = (): Promise => + new Promise((resolve) => { + chrome.storage.local.get(null, (o: any) => resolve(o)); + }); + + const clearStorage = (): Promise => + new Promise((resolve) => { + chrome.storage.local.clear(() => resolve()); + }); + + const storage = await getStorage(); + await clearStorage(); + + for (const key in storage) { + if (!storage.hasOwnProperty(key)) { + continue; + } + await this.storageService.save(key, storage[key]); + } + } + + private async buildContextMenu() { + if (!chrome.contextMenus || this.buildingContextMenu) { + return; + } + + this.buildingContextMenu = true; + await this.contextMenusRemoveAll(); + + await this.contextMenusCreate({ + type: "normal", + id: "root", + contexts: ["all"], + title: "Bitwarden", + }); + + await this.contextMenusCreate({ + type: "normal", + id: "autofill", + parentId: "root", + contexts: ["all"], + title: this.i18nService.t("autoFill"), + }); + + await this.contextMenusCreate({ + type: "normal", + id: "copy-username", + parentId: "root", + contexts: ["all"], + title: this.i18nService.t("copyUsername"), + }); + + await this.contextMenusCreate({ + type: "normal", + id: "copy-password", + parentId: "root", + contexts: ["all"], + title: this.i18nService.t("copyPassword"), + }); + + if (await this.stateService.getCanAccessPremium()) { + await this.contextMenusCreate({ + type: "normal", + id: "copy-totp", + parentId: "root", + contexts: ["all"], + title: this.i18nService.t("copyVerificationCode"), + }); + } + + await this.contextMenusCreate({ + type: "separator", + parentId: "root", + }); + + await this.contextMenusCreate({ + type: "normal", + id: "generate-password", + parentId: "root", + contexts: ["all"], + title: this.i18nService.t("generatePasswordCopied"), + }); + + await this.contextMenusCreate({ + type: "normal", + id: "copy-identifier", + parentId: "root", + contexts: ["all"], + title: this.i18nService.t("copyElementIdentifier"), + }); + + this.buildingContextMenu = false; + } + + private async contextMenuReady(tab: any, contextMenuEnabled: boolean) { + await this.loadMenuAndUpdateBadge(tab.url, tab.id, contextMenuEnabled); + this.onUpdatedRan = this.onReplacedRan = false; + } + + private async loadMenuAndUpdateBadge(url: string, tabId: number, contextMenuEnabled: boolean) { + if (!url || (!chrome.browserAction && !this.sidebarAction)) { + return; + } + + this.actionSetBadgeBackgroundColor(chrome.browserAction); + this.actionSetBadgeBackgroundColor(this.sidebarAction); + + this.menuOptionsLoaded = []; + const locked = await this.vaultTimeoutService.isLocked(); + if (!locked) { + try { + const ciphers = await this.cipherService.getAllDecryptedForUrl(url); + ciphers.sort((a: any, b: any) => this.cipherService.sortCiphersByLastUsedThenName(a, b)); + if (contextMenuEnabled) { - const authed = await this.stateService.getIsAuthenticated(); - await this.loadNoLoginsContextMenuOptions(this.i18nService.t(authed ? 'vaultLocked' : 'vaultLoggedOut')); + ciphers.forEach((cipher: any) => { + this.loadLoginContextMenuOptions(cipher); + }); } - const tabs = await BrowserApi.getActiveTabs(); - if (tabs != null) { - tabs.forEach(tab => { - if (tab.id != null) { - this.browserActionSetBadgeText('', tab.id); - this.sidebarActionSetBadgeText('', tab.id); - } - }); + const disableBadgeCounter = await this.stateService.getDisableBadgeCounter(); + let theText = ""; + + if (!disableBadgeCounter) { + if (ciphers.length > 0 && ciphers.length <= 9) { + theText = ciphers.length.toString(); + } else if (ciphers.length > 0) { + theText = "9+"; + } } + + if (contextMenuEnabled && ciphers.length === 0) { + await this.loadNoLoginsContextMenuOptions(this.i18nService.t("noMatchingLogins")); + } + + this.sidebarActionSetBadgeText(theText, tabId); + this.browserActionSetBadgeText(theText, tabId); + + return; + } catch (e) { + this.logService.error(e); + } } - private async loadLoginContextMenuOptions(cipher: any) { - if (cipher == null || cipher.type !== CipherType.Login || cipher.reprompt !== CipherRepromptType.None) { - return; - } + await this.loadMenuAndUpdateBadgeForNoAccessState(contextMenuEnabled); + } - let title = cipher.name; - if (cipher.login.username && cipher.login.username !== '') { - title += (' (' + cipher.login.username + ')'); - } - await this.loadContextMenuOptions(title, cipher.id, cipher); + private async loadMenuAndUpdateBadgeForNoAccessState(contextMenuEnabled: boolean) { + if (contextMenuEnabled) { + const authed = await this.stateService.getIsAuthenticated(); + await this.loadNoLoginsContextMenuOptions( + this.i18nService.t(authed ? "vaultLocked" : "vaultLoggedOut") + ); } - private async loadNoLoginsContextMenuOptions(noLoginsMessage: string) { - await this.loadContextMenuOptions(noLoginsMessage, 'noop', null); + const tabs = await BrowserApi.getActiveTabs(); + if (tabs != null) { + tabs.forEach((tab) => { + if (tab.id != null) { + this.browserActionSetBadgeText("", tab.id); + this.sidebarActionSetBadgeText("", tab.id); + } + }); + } + } + + private async loadLoginContextMenuOptions(cipher: any) { + if ( + cipher == null || + cipher.type !== CipherType.Login || + cipher.reprompt !== CipherRepromptType.None + ) { + return; } - private async loadContextMenuOptions(title: string, idSuffix: string, cipher: any) { - if (!chrome.contextMenus || this.menuOptionsLoaded.indexOf(idSuffix) > -1 || - (cipher != null && cipher.type !== CipherType.Login)) { - return; - } + let title = cipher.name; + if (cipher.login.username && cipher.login.username !== "") { + title += " (" + cipher.login.username + ")"; + } + await this.loadContextMenuOptions(title, cipher.id, cipher); + } - this.menuOptionsLoaded.push(idSuffix); + private async loadNoLoginsContextMenuOptions(noLoginsMessage: string) { + await this.loadContextMenuOptions(noLoginsMessage, "noop", null); + } - if (cipher == null || (cipher.login.password && cipher.login.password !== '')) { - await this.contextMenusCreate({ - type: 'normal', - id: 'autofill_' + idSuffix, - parentId: 'autofill', - contexts: ['all'], - title: this.sanitizeContextMenuTitle(title), - }); - } - - if (cipher == null || (cipher.login.username && cipher.login.username !== '')) { - await this.contextMenusCreate({ - type: 'normal', - id: 'copy-username_' + idSuffix, - parentId: 'copy-username', - contexts: ['all'], - title: this.sanitizeContextMenuTitle(title), - }); - } - - if (cipher == null || (cipher.login.password && cipher.login.password !== '' && cipher.viewPassword)) { - await this.contextMenusCreate({ - type: 'normal', - id: 'copy-password_' + idSuffix, - parentId: 'copy-password', - contexts: ['all'], - title: this.sanitizeContextMenuTitle(title), - }); - } - - const canAccessPremium = await this.stateService.getCanAccessPremium(); - if (canAccessPremium && (cipher == null || (cipher.login.totp && cipher.login.totp !== ''))) { - await this.contextMenusCreate({ - type: 'normal', - id: 'copy-totp_' + idSuffix, - parentId: 'copy-totp', - contexts: ['all'], - title: this.sanitizeContextMenuTitle(title), - }); - } + private async loadContextMenuOptions(title: string, idSuffix: string, cipher: any) { + if ( + !chrome.contextMenus || + this.menuOptionsLoaded.indexOf(idSuffix) > -1 || + (cipher != null && cipher.type !== CipherType.Login) + ) { + return; } - private sanitizeContextMenuTitle(title: string): string { - return title.replace(/&/g, '&&'); + this.menuOptionsLoaded.push(idSuffix); + + if (cipher == null || (cipher.login.password && cipher.login.password !== "")) { + await this.contextMenusCreate({ + type: "normal", + id: "autofill_" + idSuffix, + parentId: "autofill", + contexts: ["all"], + title: this.sanitizeContextMenuTitle(title), + }); } - private async fullSync(override: boolean = false) { - const syncInternal = 6 * 60 * 60 * 1000; // 6 hours - const lastSync = await this.syncService.getLastSync(); - - let lastSyncAgo = syncInternal + 1; - if (lastSync != null) { - lastSyncAgo = new Date().getTime() - lastSync.getTime(); - } - - if (override || lastSyncAgo >= syncInternal) { - await this.syncService.fullSync(override); - this.scheduleNextSync(); - } else { - this.scheduleNextSync(); - } + if (cipher == null || (cipher.login.username && cipher.login.username !== "")) { + await this.contextMenusCreate({ + type: "normal", + id: "copy-username_" + idSuffix, + parentId: "copy-username", + contexts: ["all"], + title: this.sanitizeContextMenuTitle(title), + }); } - private scheduleNextSync() { - if (this.syncTimeout) { - clearTimeout(this.syncTimeout); - } - - this.syncTimeout = setTimeout(async () => await this.fullSync(), 5 * 60 * 1000); // check every 5 minutes + if ( + cipher == null || + (cipher.login.password && cipher.login.password !== "" && cipher.viewPassword) + ) { + await this.contextMenusCreate({ + type: "normal", + id: "copy-password_" + idSuffix, + parentId: "copy-password", + contexts: ["all"], + title: this.sanitizeContextMenuTitle(title), + }); } + const canAccessPremium = await this.stateService.getCanAccessPremium(); + if (canAccessPremium && (cipher == null || (cipher.login.totp && cipher.login.totp !== ""))) { + await this.contextMenusCreate({ + type: "normal", + id: "copy-totp_" + idSuffix, + parentId: "copy-totp", + contexts: ["all"], + title: this.sanitizeContextMenuTitle(title), + }); + } + } - // Browser API Helpers + private sanitizeContextMenuTitle(title: string): string { + return title.replace(/&/g, "&&"); + } - private contextMenusRemoveAll() { - return new Promise(resolve => { - chrome.contextMenus.removeAll(() => { - resolve(); - if (chrome.runtime.lastError) { - return; - } - }); - }); + private async fullSync(override: boolean = false) { + const syncInternal = 6 * 60 * 60 * 1000; // 6 hours + const lastSync = await this.syncService.getLastSync(); + + let lastSyncAgo = syncInternal + 1; + if (lastSync != null) { + lastSyncAgo = new Date().getTime() - lastSync.getTime(); } - private contextMenusCreate(options: any) { - return new Promise(resolve => { - chrome.contextMenus.create(options, () => { - resolve(); - if (chrome.runtime.lastError) { - return; - } - }); - }); + if (override || lastSyncAgo >= syncInternal) { + await this.syncService.fullSync(override); + this.scheduleNextSync(); + } else { + this.scheduleNextSync(); + } + } + + private scheduleNextSync() { + if (this.syncTimeout) { + clearTimeout(this.syncTimeout); } - private async actionSetIcon(theAction: any, suffix: string): Promise { - if (!theAction || !theAction.setIcon) { - return; - } + this.syncTimeout = setTimeout(async () => await this.fullSync(), 5 * 60 * 1000); // check every 5 minutes + } - const options = { - path: { - 19: 'images/icon19' + suffix + '.png', - 38: 'images/icon38' + suffix + '.png', - }, - }; + // Browser API Helpers - if (this.platformUtilsService.isFirefox()) { - await theAction.setIcon(options); - } else if (this.platformUtilsService.isSafari()) { - // Workaround since Safari 14.0.3 returns a pending promise - // which doesn't resolve within a reasonable time. - theAction.setIcon(options); - } else { - return new Promise(resolve => { - theAction.setIcon(options, () => resolve()); - }); + private contextMenusRemoveAll() { + return new Promise((resolve) => { + chrome.contextMenus.removeAll(() => { + resolve(); + if (chrome.runtime.lastError) { + return; } + }); + }); + } + + private contextMenusCreate(options: any) { + return new Promise((resolve) => { + chrome.contextMenus.create(options, () => { + resolve(); + if (chrome.runtime.lastError) { + return; + } + }); + }); + } + + private async actionSetIcon(theAction: any, suffix: string): Promise { + if (!theAction || !theAction.setIcon) { + return; } - private actionSetBadgeBackgroundColor(action: any) { - if (action && action.setBadgeBackgroundColor) { - action.setBadgeBackgroundColor({ color: '#294e5f' }); - } + const options = { + path: { + 19: "images/icon19" + suffix + ".png", + 38: "images/icon38" + suffix + ".png", + }, + }; + + if (this.platformUtilsService.isFirefox()) { + await theAction.setIcon(options); + } else if (this.platformUtilsService.isSafari()) { + // Workaround since Safari 14.0.3 returns a pending promise + // which doesn't resolve within a reasonable time. + theAction.setIcon(options); + } else { + return new Promise((resolve) => { + theAction.setIcon(options, () => resolve()); + }); + } + } + + private actionSetBadgeBackgroundColor(action: any) { + if (action && action.setBadgeBackgroundColor) { + action.setBadgeBackgroundColor({ color: "#294e5f" }); + } + } + + private browserActionSetBadgeText(text: string, tabId: number) { + if (chrome.browserAction && chrome.browserAction.setBadgeText) { + chrome.browserAction.setBadgeText({ + text: text, + tabId: tabId, + }); + } + } + + private sidebarActionSetBadgeText(text: string, tabId: number) { + if (!this.sidebarAction) { + return; } - private browserActionSetBadgeText(text: string, tabId: number) { - if (chrome.browserAction && chrome.browserAction.setBadgeText) { - chrome.browserAction.setBadgeText({ - text: text, - tabId: tabId, - }); - } - } - - private sidebarActionSetBadgeText(text: string, tabId: number) { - if (!this.sidebarAction) { - return; - } - - if (this.sidebarAction.setBadgeText) { - this.sidebarAction.setBadgeText({ - text: text, - tabId: tabId, - }); - } else if (this.sidebarAction.setTitle) { - let title = 'Bitwarden'; - if (text && text !== '') { - title += (' [' + text + ']'); - } - - this.sidebarAction.setTitle({ - title: title, - tabId: tabId, - }); - } + if (this.sidebarAction.setBadgeText) { + this.sidebarAction.setBadgeText({ + text: text, + tabId: tabId, + }); + } else if (this.sidebarAction.setTitle) { + let title = "Bitwarden"; + if (text && text !== "") { + title += " [" + text + "]"; + } + + this.sidebarAction.setTitle({ + title: title, + tabId: tabId, + }); } + } } diff --git a/src/background/nativeMessaging.background.ts b/src/background/nativeMessaging.background.ts index 6858f3315d..7324ea9dae 100644 --- a/src/background/nativeMessaging.background.ts +++ b/src/background/nativeMessaging.background.ts @@ -1,331 +1,346 @@ -import { AppIdService } from 'jslib-common/abstractions/appId.service'; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StateService } from 'jslib-common/abstractions/state.service'; -import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; +import { AppIdService } from "jslib-common/abstractions/appId.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; -import { Utils } from 'jslib-common/misc/utils'; -import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey'; +import { Utils } from "jslib-common/misc/utils"; +import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey"; -import { BrowserApi } from '../browser/browserApi'; -import RuntimeBackground from './runtime.background'; +import { BrowserApi } from "../browser/browserApi"; +import RuntimeBackground from "./runtime.background"; const MessageValidTimeout = 10 * 1000; -const EncryptionAlgorithm = 'sha1'; +const EncryptionAlgorithm = "sha1"; export class NativeMessagingBackground { - private connected = false; - private connecting: boolean; - private port: browser.runtime.Port | chrome.runtime.Port; + private connected = false; + private connecting: boolean; + private port: browser.runtime.Port | chrome.runtime.Port; - private resolver: any = null; - private privateKey: ArrayBuffer = null; - private publicKey: ArrayBuffer = null; - private secureSetupResolve: any = null; - private sharedSecret: SymmetricCryptoKey; - private appId: string; - private validatingFingerprint: boolean; + private resolver: any = null; + private privateKey: ArrayBuffer = null; + private publicKey: ArrayBuffer = null; + private secureSetupResolve: any = null; + private sharedSecret: SymmetricCryptoKey; + private appId: string; + private validatingFingerprint: boolean; - constructor(private cryptoService: CryptoService, - private cryptoFunctionService: CryptoFunctionService, - private runtimeBackground: RuntimeBackground, private i18nService: I18nService, - private messagingService: MessagingService, private appIdService: AppIdService, - private platformUtilsService: PlatformUtilsService, private stateService: StateService) { - this.stateService.setBiometricFingerprintValidated(false); + constructor( + private cryptoService: CryptoService, + private cryptoFunctionService: CryptoFunctionService, + private runtimeBackground: RuntimeBackground, + private i18nService: I18nService, + private messagingService: MessagingService, + private appIdService: AppIdService, + private platformUtilsService: PlatformUtilsService, + private stateService: StateService + ) { + this.stateService.setBiometricFingerprintValidated(false); - if (chrome?.permissions?.onAdded) { - // Reload extension to activate nativeMessaging - chrome.permissions.onAdded.addListener(permissions => { - BrowserApi.reloadExtension(null); - }); - } + if (chrome?.permissions?.onAdded) { + // Reload extension to activate nativeMessaging + chrome.permissions.onAdded.addListener((permissions) => { + BrowserApi.reloadExtension(null); + }); } + } - async connect() { - this.appId = await this.appIdService.getAppId(); - this.stateService.setBiometricFingerprintValidated(false); + async connect() { + this.appId = await this.appIdService.getAppId(); + this.stateService.setBiometricFingerprintValidated(false); - return new Promise((resolve, reject) => { - this.port = BrowserApi.connectNative('com.8bit.bitwarden'); + return new Promise((resolve, reject) => { + this.port = BrowserApi.connectNative("com.8bit.bitwarden"); - this.connecting = true; + this.connecting = true; - const connectedCallback = () => { - this.connected = true; - this.connecting = false; - resolve(); - }; + const connectedCallback = () => { + this.connected = true; + this.connecting = false; + resolve(); + }; - // Safari has a bundled native component which is always available, no need to - // check if the desktop app is running. - if (this.platformUtilsService.isSafari()) { - connectedCallback(); + // Safari has a bundled native component which is always available, no need to + // check if the desktop app is running. + if (this.platformUtilsService.isSafari()) { + connectedCallback(); + } + + this.port.onMessage.addListener(async (message: any) => { + switch (message.command) { + case "connected": + connectedCallback(); + break; + case "disconnected": + if (this.connecting) { + this.messagingService.send("showDialog", { + text: this.i18nService.t("startDesktopDesc"), + title: this.i18nService.t("startDesktopTitle"), + confirmText: this.i18nService.t("ok"), + type: "error", + }); + reject(); + } + this.connected = false; + this.port.disconnect(); + break; + case "setupEncryption": + // Ignore since it belongs to another device + if (message.appId !== this.appId) { + return; } - this.port.onMessage.addListener(async (message: any) => { - switch (message.command) { - case 'connected': - connectedCallback(); - break; - case 'disconnected': - if (this.connecting) { - this.messagingService.send('showDialog', { - text: this.i18nService.t('startDesktopDesc'), - title: this.i18nService.t('startDesktopTitle'), - confirmText: this.i18nService.t('ok'), - type: 'error', - }); - reject(); - } - this.connected = false; - this.port.disconnect(); - break; - case 'setupEncryption': - // Ignore since it belongs to another device - if (message.appId !== this.appId) { - return; - } + const encrypted = Utils.fromB64ToArray(message.sharedSecret); + const decrypted = await this.cryptoFunctionService.rsaDecrypt( + encrypted.buffer, + this.privateKey, + EncryptionAlgorithm + ); - const encrypted = Utils.fromB64ToArray(message.sharedSecret); - const decrypted = await this.cryptoFunctionService.rsaDecrypt(encrypted.buffer, this.privateKey, EncryptionAlgorithm); - - if (this.validatingFingerprint) { - this.validatingFingerprint = false; - this.stateService.setBiometricFingerprintValidated(true); - } - this.sharedSecret = new SymmetricCryptoKey(decrypted); - this.secureSetupResolve(); - break; - case 'invalidateEncryption': - // Ignore since it belongs to another device - if (message.appId !== this.appId) { - return; - } - - this.sharedSecret = null; - this.privateKey = null; - this.connected = false; - - this.messagingService.send('showDialog', { - text: this.i18nService.t('nativeMessagingInvalidEncryptionDesc'), - title: this.i18nService.t('nativeMessagingInvalidEncryptionTitle'), - confirmText: this.i18nService.t('ok'), - type: 'error', - }); - break; - case 'verifyFingerprint': { - if (this.sharedSecret == null) { - this.validatingFingerprint = true; - this.showFingerprintDialog(); - } - break; - } - case 'wrongUserId': - this.showWrongUserDialog(); - default: - // Ignore since it belongs to another device - if (!this.platformUtilsService.isSafari() && message.appId !== this.appId) { - return; - } - - this.onMessage(message.message); - } - }); - - this.port.onDisconnect.addListener((p: any) => { - let error; - if (BrowserApi.isWebExtensionsApi) { - error = p.error.message; - } else { - error = chrome.runtime.lastError.message; - } - - if (error != null) { - this.messagingService.send('showDialog', { - text: this.i18nService.t('desktopIntegrationDisabledDesc'), - title: this.i18nService.t('desktopIntegrationDisabledTitle'), - confirmText: this.i18nService.t('ok'), - type: 'error', - }); - } - this.sharedSecret = null; - this.privateKey = null; - this.connected = false; - reject(); - }); - }); - } - - showWrongUserDialog() { - this.messagingService.send('showDialog', { - text: this.i18nService.t('nativeMessagingWrongUserDesc'), - title: this.i18nService.t('nativeMessagingWrongUserTitle'), - confirmText: this.i18nService.t('ok'), - type: 'error', - }); - } - - async send(message: any) { - if (!this.connected) { - await this.connect(); - } - - if (this.platformUtilsService.isSafari()) { - this.postMessage(message); - } else { - this.postMessage({ appId: this.appId, message: await this.encryptMessage(message) }); - } - } - - async encryptMessage(message: any) { - if (this.sharedSecret == null) { - await this.secureCommunication(); - } - - message.timestamp = Date.now(); - - return await this.cryptoService.encrypt(JSON.stringify(message), this.sharedSecret); - } - - getResponse(): Promise { - return new Promise((resolve, reject) => { - this.resolver = resolve; - }); - } - - private postMessage(message: any) { - // Wrap in try-catch to when the port disconnected without triggering `onDisconnect`. - try { - this.port.postMessage(message); - } catch (e) { - // tslint:disable-next-line - console.error("NativeMessaging port disconnected, disconnecting."); + if (this.validatingFingerprint) { + this.validatingFingerprint = false; + this.stateService.setBiometricFingerprintValidated(true); + } + this.sharedSecret = new SymmetricCryptoKey(decrypted); + this.secureSetupResolve(); + break; + case "invalidateEncryption": + // Ignore since it belongs to another device + if (message.appId !== this.appId) { + return; + } this.sharedSecret = null; this.privateKey = null; this.connected = false; - this.messagingService.send('showDialog', { - text: this.i18nService.t('nativeMessagingInvalidEncryptionDesc'), - title: this.i18nService.t('nativeMessagingInvalidEncryptionTitle'), - confirmText: this.i18nService.t('ok'), - type: 'error', + this.messagingService.send("showDialog", { + text: this.i18nService.t("nativeMessagingInvalidEncryptionDesc"), + title: this.i18nService.t("nativeMessagingInvalidEncryptionTitle"), + confirmText: this.i18nService.t("ok"), + type: "error", }); + break; + case "verifyFingerprint": { + if (this.sharedSecret == null) { + this.validatingFingerprint = true; + this.showFingerprintDialog(); + } + break; + } + case "wrongUserId": + this.showWrongUserDialog(); + default: + // Ignore since it belongs to another device + if (!this.platformUtilsService.isSafari() && message.appId !== this.appId) { + return; + } + + this.onMessage(message.message); } + }); + + this.port.onDisconnect.addListener((p: any) => { + let error; + if (BrowserApi.isWebExtensionsApi) { + error = p.error.message; + } else { + error = chrome.runtime.lastError.message; + } + + if (error != null) { + this.messagingService.send("showDialog", { + text: this.i18nService.t("desktopIntegrationDisabledDesc"), + title: this.i18nService.t("desktopIntegrationDisabledTitle"), + confirmText: this.i18nService.t("ok"), + type: "error", + }); + } + this.sharedSecret = null; + this.privateKey = null; + this.connected = false; + reject(); + }); + }); + } + + showWrongUserDialog() { + this.messagingService.send("showDialog", { + text: this.i18nService.t("nativeMessagingWrongUserDesc"), + title: this.i18nService.t("nativeMessagingWrongUserTitle"), + confirmText: this.i18nService.t("ok"), + type: "error", + }); + } + + async send(message: any) { + if (!this.connected) { + await this.connect(); } - private async onMessage(rawMessage: any) { - let message = rawMessage; - if (!this.platformUtilsService.isSafari()) { - message = JSON.parse(await this.cryptoService.decryptToUtf8(rawMessage, this.sharedSecret)); + if (this.platformUtilsService.isSafari()) { + this.postMessage(message); + } else { + this.postMessage({ appId: this.appId, message: await this.encryptMessage(message) }); + } + } + + async encryptMessage(message: any) { + if (this.sharedSecret == null) { + await this.secureCommunication(); + } + + message.timestamp = Date.now(); + + return await this.cryptoService.encrypt(JSON.stringify(message), this.sharedSecret); + } + + getResponse(): Promise { + return new Promise((resolve, reject) => { + this.resolver = resolve; + }); + } + + private postMessage(message: any) { + // Wrap in try-catch to when the port disconnected without triggering `onDisconnect`. + try { + this.port.postMessage(message); + } catch (e) { + // tslint:disable-next-line + console.error("NativeMessaging port disconnected, disconnecting."); + + this.sharedSecret = null; + this.privateKey = null; + this.connected = false; + + this.messagingService.send("showDialog", { + text: this.i18nService.t("nativeMessagingInvalidEncryptionDesc"), + title: this.i18nService.t("nativeMessagingInvalidEncryptionTitle"), + confirmText: this.i18nService.t("ok"), + type: "error", + }); + } + } + + private async onMessage(rawMessage: any) { + let message = rawMessage; + if (!this.platformUtilsService.isSafari()) { + message = JSON.parse(await this.cryptoService.decryptToUtf8(rawMessage, this.sharedSecret)); + } + + if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) { + // tslint:disable-next-line + console.error("NativeMessage is to old, ignoring."); + return; + } + + switch (message.command) { + case "biometricUnlock": + await this.stateService.setBiometricAwaitingAcceptance(null); + + if (message.response === "not enabled") { + this.messagingService.send("showDialog", { + text: this.i18nService.t("biometricsNotEnabledDesc"), + title: this.i18nService.t("biometricsNotEnabledTitle"), + confirmText: this.i18nService.t("ok"), + type: "error", + }); + break; + } else if (message.response === "not supported") { + this.messagingService.send("showDialog", { + text: this.i18nService.t("biometricsNotSupportedDesc"), + title: this.i18nService.t("biometricsNotSupportedTitle"), + confirmText: this.i18nService.t("ok"), + type: "error", + }); + break; } - if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) { + const enabled = await this.stateService.getBiometricUnlock(); + if (enabled === null || enabled === false) { + if (message.response === "unlocked") { + await this.stateService.setBiometricUnlock(true); + } + break; + } + + // Ignore unlock if already unlockeded + if (!this.stateService.getBiometricLocked()) { + break; + } + + if (message.response === "unlocked") { + await this.cryptoService.setKey( + new SymmetricCryptoKey(Utils.fromB64ToArray(message.keyB64).buffer) + ); + + // Verify key is correct by attempting to decrypt a secret + try { + await this.cryptoService.getFingerprint(await this.stateService.getUserId()); + } catch (e) { // tslint:disable-next-line - console.error('NativeMessage is to old, ignoring.'); - return; - } - - switch (message.command) { - case 'biometricUnlock': - await this.stateService.setBiometricAwaitingAcceptance(null); - - if (message.response === 'not enabled') { - this.messagingService.send('showDialog', { - text: this.i18nService.t('biometricsNotEnabledDesc'), - title: this.i18nService.t('biometricsNotEnabledTitle'), - confirmText: this.i18nService.t('ok'), - type: 'error', - }); - break; - } else if (message.response === 'not supported') { - this.messagingService.send('showDialog', { - text: this.i18nService.t('biometricsNotSupportedDesc'), - title: this.i18nService.t('biometricsNotSupportedTitle'), - confirmText: this.i18nService.t('ok'), - type: 'error', - }); - break; - } - - const enabled = await this.stateService.getBiometricUnlock(); - if (enabled === null || enabled === false) { - if (message.response === 'unlocked') { - await this.stateService.setBiometricUnlock(true); - } - break; - } - - // Ignore unlock if already unlockeded - if (!this.stateService.getBiometricLocked()) { - break; - } - - if (message.response === 'unlocked') { - await this.cryptoService.setKey(new SymmetricCryptoKey(Utils.fromB64ToArray(message.keyB64).buffer)); - - // Verify key is correct by attempting to decrypt a secret - try { - await this.cryptoService.getFingerprint(await this.stateService.getUserId()); - } catch (e) { - // tslint:disable-next-line - console.error('Unable to verify key:', e); - await this.cryptoService.clearKey(); - this.showWrongUserDialog(); - - message = false; - break; - } - - await this.stateService.setBiometricLocked(false); - this.runtimeBackground.processMessage({ command: 'unlocked' }, null, null); - } - break; - default: - // tslint:disable-next-line - console.error('NativeMessage, got unknown command: ', message.command); - } - - if (this.resolver) { - this.resolver(message); + console.error("Unable to verify key:", e); + await this.cryptoService.clearKey(); + this.showWrongUserDialog(); + + message = false; + break; + } + + await this.stateService.setBiometricLocked(false); + this.runtimeBackground.processMessage({ command: "unlocked" }, null, null); } + break; + default: + // tslint:disable-next-line + console.error("NativeMessage, got unknown command: ", message.command); } - private async secureCommunication() { - const [publicKey, privateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); - this.publicKey = publicKey; - this.privateKey = privateKey; + if (this.resolver) { + this.resolver(message); + } + } - this.sendUnencrypted({ - command: 'setupEncryption', - publicKey: Utils.fromBufferToB64(publicKey), - userId: await this.stateService.getUserId(), - }); + private async secureCommunication() { + const [publicKey, privateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); + this.publicKey = publicKey; + this.privateKey = privateKey; - return new Promise((resolve, reject) => this.secureSetupResolve = resolve); + this.sendUnencrypted({ + command: "setupEncryption", + publicKey: Utils.fromBufferToB64(publicKey), + userId: await this.stateService.getUserId(), + }); + + return new Promise((resolve, reject) => (this.secureSetupResolve = resolve)); + } + + private async sendUnencrypted(message: any) { + if (!this.connected) { + await this.connect(); } - private async sendUnencrypted(message: any) { - if (!this.connected) { - await this.connect(); - } + message.timestamp = Date.now(); - message.timestamp = Date.now(); + this.postMessage({ appId: this.appId, message: message }); + } - this.postMessage({ appId: this.appId, message: message }); - } + private async showFingerprintDialog() { + const fingerprint = ( + await this.cryptoService.getFingerprint(await this.stateService.getUserId(), this.publicKey) + ).join(" "); - private async showFingerprintDialog() { - const fingerprint = (await this.cryptoService.getFingerprint(await this.stateService.getUserId(), this.publicKey)).join(' '); - - this.messagingService.send('showDialog', { - html: `${this.i18nService.t('desktopIntegrationVerificationText')}

${fingerprint}`, - title: this.i18nService.t('desktopSyncVerificationTitle'), - confirmText: this.i18nService.t('ok'), - type: 'warning', - }); - } + this.messagingService.send("showDialog", { + html: `${this.i18nService.t( + "desktopIntegrationVerificationText" + )}

${fingerprint}`, + title: this.i18nService.t("desktopSyncVerificationTitle"), + confirmText: this.i18nService.t("ok"), + type: "warning", + }); + } } diff --git a/src/background/notification.background.ts b/src/background/notification.background.ts index d67f40d2f9..b365241bb4 100644 --- a/src/background/notification.background.ts +++ b/src/background/notification.background.ts @@ -28,505 +28,423 @@ import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNoti import { NotificationQueueMessageType } from "./models/notificationQueueMessageType"; export default class NotificationBackground { - private notificationQueue: ( - | AddLoginQueueMessage - | AddChangePasswordQueueMessage - )[] = []; + private notificationQueue: (AddLoginQueueMessage | AddChangePasswordQueueMessage)[] = []; - constructor( - private main: MainBackground, - private autofillService: AutofillService, - private cipherService: CipherService, - private storageService: StorageService, - private vaultTimeoutService: VaultTimeoutService, - private policyService: PolicyService, - private folderService: FolderService, - private stateService: StateService - ) {} + constructor( + private main: MainBackground, + private autofillService: AutofillService, + private cipherService: CipherService, + private storageService: StorageService, + private vaultTimeoutService: VaultTimeoutService, + private policyService: PolicyService, + private folderService: FolderService, + private stateService: StateService + ) {} - async init() { - if (chrome.runtime == null) { - return; - } - - BrowserApi.messageListener( - "notification.background", - async (msg: any, sender: chrome.runtime.MessageSender) => { - await this.processMessage(msg, sender); - } - ); - - this.cleanupNotificationQueue(); + async init() { + if (chrome.runtime == null) { + return; } - async processMessage(msg: any, sender: chrome.runtime.MessageSender) { - switch (msg.command) { - case "unlockCompleted": - if (msg.data.target !== "notification.background") { - return; - } - await this.processMessage( - msg.data.commandToRetry.msg, - msg.data.commandToRetry.sender - ); - break; - case "bgGetDataForTab": - await this.getDataForTab(sender.tab, msg.responseCommand); - break; - case "bgCloseNotificationBar": - await BrowserApi.tabSendMessageData( - sender.tab, - "closeNotificationBar" - ); - break; - case "bgAdjustNotificationBar": - await BrowserApi.tabSendMessageData( - sender.tab, - "adjustNotificationBar", - msg.data - ); - break; - case "bgAddLogin": - await this.addLogin(msg.login, sender.tab); - break; - case "bgChangedPassword": - await this.changedPassword(msg.data, sender.tab); - break; - case "bgAddClose": - case "bgChangeClose": - this.removeTabFromNotificationQueue(sender.tab); - break; - case "bgAddSave": - case "bgChangeSave": - if (await this.vaultTimeoutService.isLocked()) { - const retryMessage: LockedVaultPendingNotificationsItem = { - commandToRetry: { - msg: msg, - sender: sender, - }, - target: "notification.background", - }; - await BrowserApi.tabSendMessageData( - sender.tab, - "addToLockedVaultPendingNotifications", - retryMessage - ); - await BrowserApi.tabSendMessageData( - sender.tab, - "promptForLogin" - ); - return; - } - await this.saveOrUpdateCredentials(sender.tab, msg.folder); - break; - case "bgNeverSave": - await this.saveNever(sender.tab); - break; - case "collectPageDetailsResponse": - switch (msg.sender) { - case "notificationBar": - const forms = - this.autofillService.getFormsWithPasswordFields( - msg.details - ); - await BrowserApi.tabSendMessageData( - msg.tab, - "notificationBarPageDetails", - { - details: msg.details, - forms: forms, - } - ); - break; - default: - break; - } - break; - default: - break; + BrowserApi.messageListener( + "notification.background", + async (msg: any, sender: chrome.runtime.MessageSender) => { + await this.processMessage(msg, sender); + } + ); + + this.cleanupNotificationQueue(); + } + + async processMessage(msg: any, sender: chrome.runtime.MessageSender) { + switch (msg.command) { + case "unlockCompleted": + if (msg.data.target !== "notification.background") { + return; } - } - - async checkNotificationQueue(tab: chrome.tabs.Tab = null): Promise { - if (this.notificationQueue.length === 0) { - return; + await this.processMessage(msg.data.commandToRetry.msg, msg.data.commandToRetry.sender); + break; + case "bgGetDataForTab": + await this.getDataForTab(sender.tab, msg.responseCommand); + break; + case "bgCloseNotificationBar": + await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar"); + break; + case "bgAdjustNotificationBar": + await BrowserApi.tabSendMessageData(sender.tab, "adjustNotificationBar", msg.data); + break; + case "bgAddLogin": + await this.addLogin(msg.login, sender.tab); + break; + case "bgChangedPassword": + await this.changedPassword(msg.data, sender.tab); + break; + case "bgAddClose": + case "bgChangeClose": + this.removeTabFromNotificationQueue(sender.tab); + break; + case "bgAddSave": + case "bgChangeSave": + if (await this.vaultTimeoutService.isLocked()) { + const retryMessage: LockedVaultPendingNotificationsItem = { + commandToRetry: { + msg: msg, + sender: sender, + }, + target: "notification.background", + }; + await BrowserApi.tabSendMessageData( + sender.tab, + "addToLockedVaultPendingNotifications", + retryMessage + ); + await BrowserApi.tabSendMessageData(sender.tab, "promptForLogin"); + return; } - - if (tab != null) { - this.doNotificationQueueCheck(tab); - return; - } - - const currentTab = await BrowserApi.getTabFromCurrentWindow(); - if (currentTab != null) { - this.doNotificationQueueCheck(currentTab); - } - } - - private cleanupNotificationQueue() { - for (let i = this.notificationQueue.length - 1; i >= 0; i--) { - if (this.notificationQueue[i].expires < new Date()) { - this.notificationQueue.splice(i, 1); - } - } - setTimeout(() => this.cleanupNotificationQueue(), 2 * 60 * 1000); // check every 2 minutes - } - - private doNotificationQueueCheck(tab: chrome.tabs.Tab): void { - if (tab == null) { - return; - } - - const tabDomain = Utils.getDomain(tab.url); - if (tabDomain == null) { - return; - } - - for (let i = 0; i < this.notificationQueue.length; i++) { - if ( - this.notificationQueue[i].tabId !== tab.id || - this.notificationQueue[i].domain !== tabDomain - ) { - continue; - } - - if ( - this.notificationQueue[i].type === - NotificationQueueMessageType.addLogin - ) { - BrowserApi.tabSendMessageData(tab, "openNotificationBar", { - type: "add", - typeData: { - isVaultLocked: this.notificationQueue[i].wasVaultLocked, - }, - }); - } else if ( - this.notificationQueue[i].type === - NotificationQueueMessageType.changePassword - ) { - BrowserApi.tabSendMessageData(tab, "openNotificationBar", { - type: "change", - typeData: { - isVaultLocked: this.notificationQueue[i].wasVaultLocked, - }, - }); - } + await this.saveOrUpdateCredentials(sender.tab, msg.folder); + break; + case "bgNeverSave": + await this.saveNever(sender.tab); + break; + case "collectPageDetailsResponse": + switch (msg.sender) { + case "notificationBar": + const forms = this.autofillService.getFormsWithPasswordFields(msg.details); + await BrowserApi.tabSendMessageData(msg.tab, "notificationBarPageDetails", { + details: msg.details, + forms: forms, + }); + break; + default: break; } + break; + default: + break; + } + } + + async checkNotificationQueue(tab: chrome.tabs.Tab = null): Promise { + if (this.notificationQueue.length === 0) { + return; } - private removeTabFromNotificationQueue(tab: chrome.tabs.Tab) { - for (let i = this.notificationQueue.length - 1; i >= 0; i--) { - if (this.notificationQueue[i].tabId === tab.id) { - this.notificationQueue.splice(i, 1); - } - } + if (tab != null) { + this.doNotificationQueueCheck(tab); + return; } - private async addLogin( - loginInfo: AddLoginRuntimeMessage, - tab: chrome.tabs.Tab + const currentTab = await BrowserApi.getTabFromCurrentWindow(); + if (currentTab != null) { + this.doNotificationQueueCheck(currentTab); + } + } + + private cleanupNotificationQueue() { + for (let i = this.notificationQueue.length - 1; i >= 0; i--) { + if (this.notificationQueue[i].expires < new Date()) { + this.notificationQueue.splice(i, 1); + } + } + setTimeout(() => this.cleanupNotificationQueue(), 2 * 60 * 1000); // check every 2 minutes + } + + private doNotificationQueueCheck(tab: chrome.tabs.Tab): void { + if (tab == null) { + return; + } + + const tabDomain = Utils.getDomain(tab.url); + if (tabDomain == null) { + return; + } + + for (let i = 0; i < this.notificationQueue.length; i++) { + if ( + this.notificationQueue[i].tabId !== tab.id || + this.notificationQueue[i].domain !== tabDomain + ) { + continue; + } + + if (this.notificationQueue[i].type === NotificationQueueMessageType.addLogin) { + BrowserApi.tabSendMessageData(tab, "openNotificationBar", { + type: "add", + typeData: { + isVaultLocked: this.notificationQueue[i].wasVaultLocked, + }, + }); + } else if (this.notificationQueue[i].type === NotificationQueueMessageType.changePassword) { + BrowserApi.tabSendMessageData(tab, "openNotificationBar", { + type: "change", + typeData: { + isVaultLocked: this.notificationQueue[i].wasVaultLocked, + }, + }); + } + break; + } + } + + private removeTabFromNotificationQueue(tab: chrome.tabs.Tab) { + for (let i = this.notificationQueue.length - 1; i >= 0; i--) { + if (this.notificationQueue[i].tabId === tab.id) { + this.notificationQueue.splice(i, 1); + } + } + } + + private async addLogin(loginInfo: AddLoginRuntimeMessage, tab: chrome.tabs.Tab) { + if (!(await this.stateService.getIsAuthenticated())) { + return; + } + + const loginDomain = Utils.getDomain(loginInfo.url); + if (loginDomain == null) { + return; + } + + let normalizedUsername = loginInfo.username; + if (normalizedUsername != null) { + normalizedUsername = normalizedUsername.toLowerCase(); + } + + if (await this.vaultTimeoutService.isLocked()) { + if (!(await this.allowPersonalOwnership())) { + return; + } + + this.pushAddLoginToQueue(loginDomain, loginInfo, tab, true); + return; + } + + const ciphers = await this.cipherService.getAllDecryptedForUrl(loginInfo.url); + const usernameMatches = ciphers.filter( + (c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername + ); + if (usernameMatches.length === 0) { + const disabledAddLogin = await this.stateService.getDisableAddLoginNotification(); + if (disabledAddLogin) { + return; + } + + if (!(await this.allowPersonalOwnership())) { + return; + } + + this.pushAddLoginToQueue(loginDomain, loginInfo, tab); + } else if ( + usernameMatches.length === 1 && + usernameMatches[0].login.password !== loginInfo.password ) { - if (!(await this.stateService.getIsAuthenticated())) { - return; + const disabledChangePassword = + await this.stateService.getDisableChangedPasswordNotification(); + if (disabledChangePassword) { + return; + } + this.pushChangePasswordToQueue(usernameMatches[0].id, loginDomain, loginInfo.password, tab); + } + } + + private async pushAddLoginToQueue( + loginDomain: string, + loginInfo: AddLoginRuntimeMessage, + tab: chrome.tabs.Tab, + isVaultLocked: boolean = false + ) { + // remove any old messages for this tab + this.removeTabFromNotificationQueue(tab); + const message: AddLoginQueueMessage = { + type: NotificationQueueMessageType.addLogin, + username: loginInfo.username, + password: loginInfo.password, + domain: loginDomain, + uri: loginInfo.url, + tabId: tab.id, + expires: new Date(new Date().getTime() + 5 * 60000), // 5 minutes + wasVaultLocked: isVaultLocked, + }; + this.notificationQueue.push(message); + await this.checkNotificationQueue(tab); + } + + private async changedPassword(changeData: ChangePasswordRuntimeMessage, tab: chrome.tabs.Tab) { + const loginDomain = Utils.getDomain(changeData.url); + if (loginDomain == null) { + return; + } + + if (await this.vaultTimeoutService.isLocked()) { + this.pushChangePasswordToQueue(null, loginDomain, changeData.newPassword, tab, true); + return; + } + + let id: string = null; + const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url); + if (changeData.currentPassword != null) { + const passwordMatches = ciphers.filter( + (c) => c.login.password === changeData.currentPassword + ); + if (passwordMatches.length === 1) { + id = passwordMatches[0].id; + } + } else if (ciphers.length === 1) { + id = ciphers[0].id; + } + if (id != null) { + this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, tab); + } + } + + private async pushChangePasswordToQueue( + cipherId: string, + loginDomain: string, + newPassword: string, + tab: chrome.tabs.Tab, + isVaultLocked: boolean = false + ) { + // remove any old messages for this tab + this.removeTabFromNotificationQueue(tab); + const message: AddChangePasswordQueueMessage = { + type: NotificationQueueMessageType.changePassword, + cipherId: cipherId, + newPassword: newPassword, + domain: loginDomain, + tabId: tab.id, + expires: new Date(new Date().getTime() + 5 * 60000), // 5 minutes + wasVaultLocked: isVaultLocked, + }; + this.notificationQueue.push(message); + await this.checkNotificationQueue(tab); + } + + private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, folderId?: string) { + for (let i = this.notificationQueue.length - 1; i >= 0; i--) { + const queueMessage = this.notificationQueue[i]; + if ( + queueMessage.tabId !== tab.id || + (queueMessage.type !== NotificationQueueMessageType.addLogin && + queueMessage.type !== NotificationQueueMessageType.changePassword) + ) { + continue; + } + + const tabDomain = Utils.getDomain(tab.url); + if (tabDomain != null && tabDomain !== queueMessage.domain) { + continue; + } + + this.notificationQueue.splice(i, 1); + BrowserApi.tabSendMessageData(tab, "closeNotificationBar"); + + if (queueMessage.type === NotificationQueueMessageType.changePassword) { + const message = queueMessage as AddChangePasswordQueueMessage; + const cipher = await this.getDecryptedCipherById(message.cipherId); + if (cipher == null) { + return; } + await this.updateCipher(cipher, message.newPassword); + return; + } - const loginDomain = Utils.getDomain(loginInfo.url); - if (loginDomain == null) { - return; - } + if (!queueMessage.wasVaultLocked) { + await this.createNewCipher(queueMessage as AddLoginQueueMessage, folderId); + } - let normalizedUsername = loginInfo.username; - if (normalizedUsername != null) { - normalizedUsername = normalizedUsername.toLowerCase(); - } - - if (await this.vaultTimeoutService.isLocked()) { - if (!(await this.allowPersonalOwnership())) { - return; - } - - this.pushAddLoginToQueue(loginDomain, loginInfo, tab, true); - return; - } - - const ciphers = await this.cipherService.getAllDecryptedForUrl( - loginInfo.url - ); + // If the vault was locked, check if a cipher needs updating instead of creating a new one + if ( + queueMessage.type === NotificationQueueMessageType.addLogin && + queueMessage.wasVaultLocked === true + ) { + const message = queueMessage as AddLoginQueueMessage; + const ciphers = await this.cipherService.getAllDecryptedForUrl(message.uri); const usernameMatches = ciphers.filter( - (c) => - c.login.username != null && - c.login.username.toLowerCase() === normalizedUsername + (c) => c.login.username != null && c.login.username.toLowerCase() === message.username ); - if (usernameMatches.length === 0) { - const disabledAddLogin = - await this.stateService.getDisableAddLoginNotification(); - if (disabledAddLogin) { - return; - } - if (!(await this.allowPersonalOwnership())) { - return; - } - - this.pushAddLoginToQueue(loginDomain, loginInfo, tab); - } else if ( - usernameMatches.length === 1 && - usernameMatches[0].login.password !== loginInfo.password - ) { - const disabledChangePassword = - await this.stateService.getDisableChangedPasswordNotification(); - if (disabledChangePassword) { - return; - } - this.pushChangePasswordToQueue( - usernameMatches[0].id, - loginDomain, - loginInfo.password, - tab - ); - } - } - - private async pushAddLoginToQueue( - loginDomain: string, - loginInfo: AddLoginRuntimeMessage, - tab: chrome.tabs.Tab, - isVaultLocked: boolean = false - ) { - // remove any old messages for this tab - this.removeTabFromNotificationQueue(tab); - const message: AddLoginQueueMessage = { - type: NotificationQueueMessageType.addLogin, - username: loginInfo.username, - password: loginInfo.password, - domain: loginDomain, - uri: loginInfo.url, - tabId: tab.id, - expires: new Date(new Date().getTime() + 5 * 60000), // 5 minutes - wasVaultLocked: isVaultLocked, - }; - this.notificationQueue.push(message); - await this.checkNotificationQueue(tab); - } - - private async changedPassword( - changeData: ChangePasswordRuntimeMessage, - tab: chrome.tabs.Tab - ) { - const loginDomain = Utils.getDomain(changeData.url); - if (loginDomain == null) { - return; + if (usernameMatches.length >= 1) { + await this.updateCipher(usernameMatches[0], message.password); + return; } - if (await this.vaultTimeoutService.isLocked()) { - this.pushChangePasswordToQueue( - null, - loginDomain, - changeData.newPassword, - tab, - true - ); - return; - } + await this.createNewCipher(message, folderId); + } + } + } - let id: string = null; - const ciphers = await this.cipherService.getAllDecryptedForUrl( - changeData.url - ); - if (changeData.currentPassword != null) { - const passwordMatches = ciphers.filter( - (c) => c.login.password === changeData.currentPassword - ); - if (passwordMatches.length === 1) { - id = passwordMatches[0].id; - } - } else if (ciphers.length === 1) { - id = ciphers[0].id; - } - if (id != null) { - this.pushChangePasswordToQueue( - id, - loginDomain, - changeData.newPassword, - tab - ); - } + private async createNewCipher(queueMessage: AddLoginQueueMessage, folderId: string) { + const loginModel = new LoginView(); + const loginUri = new LoginUriView(); + loginUri.uri = queueMessage.uri; + loginModel.uris = [loginUri]; + loginModel.username = queueMessage.username; + loginModel.password = queueMessage.password; + const model = new CipherView(); + model.name = Utils.getHostname(queueMessage.uri) || queueMessage.domain; + model.name = model.name.replace(/^www\./, ""); + model.type = CipherType.Login; + model.login = loginModel; + + if (!Utils.isNullOrWhitespace(folderId)) { + const folders = await this.folderService.getAllDecrypted(); + if (folders.some((x) => x.id === folderId)) { + model.folderId = folderId; + } } - private async pushChangePasswordToQueue( - cipherId: string, - loginDomain: string, - newPassword: string, - tab: chrome.tabs.Tab, - isVaultLocked: boolean = false - ) { - // remove any old messages for this tab - this.removeTabFromNotificationQueue(tab); - const message: AddChangePasswordQueueMessage = { - type: NotificationQueueMessageType.changePassword, - cipherId: cipherId, - newPassword: newPassword, - domain: loginDomain, - tabId: tab.id, - expires: new Date(new Date().getTime() + 5 * 60000), // 5 minutes - wasVaultLocked: isVaultLocked, - }; - this.notificationQueue.push(message); - await this.checkNotificationQueue(tab); + const cipher = await this.cipherService.encrypt(model); + await this.cipherService.saveWithServer(cipher); + } + + private async getDecryptedCipherById(cipherId: string) { + const cipher = await this.cipherService.get(cipherId); + if (cipher != null && cipher.type === CipherType.Login) { + return await cipher.decrypt(); + } + return null; + } + + private async updateCipher(cipher: CipherView, newPassword: string) { + if (cipher != null && cipher.type === CipherType.Login) { + cipher.login.password = newPassword; + const newCipher = await this.cipherService.encrypt(cipher); + await this.cipherService.saveWithServer(newCipher); + } + } + + private async saveNever(tab: chrome.tabs.Tab) { + for (let i = this.notificationQueue.length - 1; i >= 0; i--) { + const queueMessage = this.notificationQueue[i]; + if ( + queueMessage.tabId !== tab.id || + queueMessage.type !== NotificationQueueMessageType.addLogin + ) { + continue; + } + + const tabDomain = Utils.getDomain(tab.url); + if (tabDomain != null && tabDomain !== queueMessage.domain) { + continue; + } + + this.notificationQueue.splice(i, 1); + BrowserApi.tabSendMessageData(tab, "closeNotificationBar"); + + const hostname = Utils.getHostname(tab.url); + await this.cipherService.saveNeverDomain(hostname); + } + } + + private async getDataForTab(tab: chrome.tabs.Tab, responseCommand: string) { + const responseData: any = {}; + if (responseCommand === "notificationBarGetFoldersList") { + responseData.folders = await this.folderService.getAllDecrypted(); } - private async saveOrUpdateCredentials( - tab: chrome.tabs.Tab, - folderId?: string - ) { - for (let i = this.notificationQueue.length - 1; i >= 0; i--) { - const queueMessage = this.notificationQueue[i]; - if ( - queueMessage.tabId !== tab.id || - (queueMessage.type !== NotificationQueueMessageType.addLogin && - queueMessage.type !== - NotificationQueueMessageType.changePassword) - ) { - continue; - } + await BrowserApi.tabSendMessageData(tab, responseCommand, responseData); + } - const tabDomain = Utils.getDomain(tab.url); - if (tabDomain != null && tabDomain !== queueMessage.domain) { - continue; - } - - this.notificationQueue.splice(i, 1); - BrowserApi.tabSendMessageData(tab, "closeNotificationBar"); - - if ( - queueMessage.type === - NotificationQueueMessageType.changePassword - ) { - const message = queueMessage as AddChangePasswordQueueMessage; - const cipher = await this.getDecryptedCipherById( - message.cipherId - ); - if (cipher == null) { - return; - } - await this.updateCipher(cipher, message.newPassword); - return; - } - - if (!queueMessage.wasVaultLocked) { - await this.createNewCipher( - queueMessage as AddLoginQueueMessage, - folderId - ); - } - - // If the vault was locked, check if a cipher needs updating instead of creating a new one - if ( - queueMessage.type === NotificationQueueMessageType.addLogin && - queueMessage.wasVaultLocked === true - ) { - const message = queueMessage as AddLoginQueueMessage; - const ciphers = await this.cipherService.getAllDecryptedForUrl( - message.uri - ); - const usernameMatches = ciphers.filter( - (c) => - c.login.username != null && - c.login.username.toLowerCase() === message.username - ); - - if (usernameMatches.length >= 1) { - await this.updateCipher( - usernameMatches[0], - message.password - ); - return; - } - - await this.createNewCipher(message, folderId); - } - } - } - - private async createNewCipher( - queueMessage: AddLoginQueueMessage, - folderId: string - ) { - const loginModel = new LoginView(); - const loginUri = new LoginUriView(); - loginUri.uri = queueMessage.uri; - loginModel.uris = [loginUri]; - loginModel.username = queueMessage.username; - loginModel.password = queueMessage.password; - const model = new CipherView(); - model.name = Utils.getHostname(queueMessage.uri) || queueMessage.domain; - model.name = model.name.replace(/^www\./, ""); - model.type = CipherType.Login; - model.login = loginModel; - - if (!Utils.isNullOrWhitespace(folderId)) { - const folders = await this.folderService.getAllDecrypted(); - if (folders.some((x) => x.id === folderId)) { - model.folderId = folderId; - } - } - - const cipher = await this.cipherService.encrypt(model); - await this.cipherService.saveWithServer(cipher); - } - - private async getDecryptedCipherById(cipherId: string) { - const cipher = await this.cipherService.get(cipherId); - if (cipher != null && cipher.type === CipherType.Login) { - return await cipher.decrypt(); - } - return null; - } - - private async updateCipher(cipher: CipherView, newPassword: string) { - if (cipher != null && cipher.type === CipherType.Login) { - cipher.login.password = newPassword; - const newCipher = await this.cipherService.encrypt(cipher); - await this.cipherService.saveWithServer(newCipher); - } - } - - private async saveNever(tab: chrome.tabs.Tab) { - for (let i = this.notificationQueue.length - 1; i >= 0; i--) { - const queueMessage = this.notificationQueue[i]; - if ( - queueMessage.tabId !== tab.id || - queueMessage.type !== NotificationQueueMessageType.addLogin - ) { - continue; - } - - const tabDomain = Utils.getDomain(tab.url); - if (tabDomain != null && tabDomain !== queueMessage.domain) { - continue; - } - - this.notificationQueue.splice(i, 1); - BrowserApi.tabSendMessageData(tab, "closeNotificationBar"); - - const hostname = Utils.getHostname(tab.url); - await this.cipherService.saveNeverDomain(hostname); - } - } - - private async getDataForTab(tab: chrome.tabs.Tab, responseCommand: string) { - const responseData: any = {}; - if (responseCommand === "notificationBarGetFoldersList") { - responseData.folders = await this.folderService.getAllDecrypted(); - } - - await BrowserApi.tabSendMessageData(tab, responseCommand, responseData); - } - - private async allowPersonalOwnership(): Promise { - return !(await this.policyService.policyAppliesToUser( - PolicyType.PersonalOwnership - )); - } + private async allowPersonalOwnership(): Promise { + return !(await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership)); + } } diff --git a/src/background/runtime.background.ts b/src/background/runtime.background.ts index c5fc6b2d74..24c5adeb12 100644 --- a/src/background/runtime.background.ts +++ b/src/background/runtime.background.ts @@ -1,220 +1,245 @@ -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { NotificationsService } from 'jslib-common/abstractions/notifications.service'; -import { SystemService } from 'jslib-common/abstractions/system.service'; -import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { NotificationsService } from "jslib-common/abstractions/notifications.service"; +import { SystemService } from "jslib-common/abstractions/system.service"; +import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; -import { AutofillService } from '../services/abstractions/autofill.service'; -import BrowserPlatformUtilsService from '../services/browserPlatformUtils.service'; +import { AutofillService } from "../services/abstractions/autofill.service"; +import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service"; -import { BrowserApi } from '../browser/browserApi'; +import { BrowserApi } from "../browser/browserApi"; -import MainBackground from './main.background'; +import MainBackground from "./main.background"; -import { Utils } from 'jslib-common/misc/utils'; +import { Utils } from "jslib-common/misc/utils"; -import { StateService } from 'jslib-common/abstractions/state.service'; -import LockedVaultPendingNotificationsItem from './models/lockedVaultPendingNotificationsItem'; +import { StateService } from "jslib-common/abstractions/state.service"; +import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNotificationsItem"; export default class RuntimeBackground { - private autofillTimeout: any; - private pageDetailsToAutoFill: any[] = []; - private onInstalledReason: string = null; - private lockedVaultPendingNotifications: LockedVaultPendingNotificationsItem[] = []; + private autofillTimeout: any; + private pageDetailsToAutoFill: any[] = []; + private onInstalledReason: string = null; + private lockedVaultPendingNotifications: LockedVaultPendingNotificationsItem[] = []; - constructor(private main: MainBackground, private autofillService: AutofillService, - private platformUtilsService: BrowserPlatformUtilsService, - private i18nService: I18nService, - private notificationsService: NotificationsService, private systemService: SystemService, - private environmentService: EnvironmentService, private messagingService: MessagingService, - private stateService: StateService, private logService: LogService) { + constructor( + private main: MainBackground, + private autofillService: AutofillService, + private platformUtilsService: BrowserPlatformUtilsService, + private i18nService: I18nService, + private notificationsService: NotificationsService, + private systemService: SystemService, + private environmentService: EnvironmentService, + private messagingService: MessagingService, + private stateService: StateService, + private logService: LogService + ) { + // onInstalled listener must be wired up before anything else, so we do it in the ctor + chrome.runtime.onInstalled.addListener((details: any) => { + this.onInstalledReason = details.reason; + }); + } - // onInstalled listener must be wired up before anything else, so we do it in the ctor - chrome.runtime.onInstalled.addListener((details: any) => { - this.onInstalledReason = details.reason; - }); + async init() { + if (!chrome.runtime) { + return; } - async init() { - if (!chrome.runtime) { - return; + await this.checkOnInstalled(); + BrowserApi.messageListener( + "runtime.background", + async (msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) => { + await this.processMessage(msg, sender, sendResponse); + } + ); + } + + async processMessage(msg: any, sender: any, sendResponse: any) { + switch (msg.command) { + case "loggedIn": + case "unlocked": + let item: LockedVaultPendingNotificationsItem; + + if (this.lockedVaultPendingNotifications.length > 0) { + await BrowserApi.closeLoginTab(); + + item = this.lockedVaultPendingNotifications.pop(); + if (item.commandToRetry.sender?.tab?.id) { + await BrowserApi.focusSpecifiedTab(item.commandToRetry.sender.tab.id); + } } - await this.checkOnInstalled(); - BrowserApi.messageListener('runtime.background', async (msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) => { - await this.processMessage(msg, sender, sendResponse); - }); - } + await this.main.setIcon(); + await this.main.refreshBadgeAndMenu(false); + this.notificationsService.updateConnection(msg.command === "unlocked"); + this.systemService.cancelProcessReload(); - async processMessage(msg: any, sender: any, sendResponse: any) { - switch (msg.command) { - case 'loggedIn': - case 'unlocked': - let item: LockedVaultPendingNotificationsItem; - - if (this.lockedVaultPendingNotifications.length > 0) { - await BrowserApi.closeLoginTab(); - - item = this.lockedVaultPendingNotifications.pop(); - if (item.commandToRetry.sender?.tab?.id) { - await BrowserApi.focusSpecifiedTab(item.commandToRetry.sender.tab.id); - } - } - - await this.main.setIcon(); - await this.main.refreshBadgeAndMenu(false); - this.notificationsService.updateConnection(msg.command === 'unlocked'); - this.systemService.cancelProcessReload(); - - if (item) { - await BrowserApi.tabSendMessageData(item.commandToRetry.sender.tab, 'unlockCompleted', item); - } - break; - case 'addToLockedVaultPendingNotifications': - this.lockedVaultPendingNotifications.push(msg.data); - break; - case 'logout': - await this.main.logout(msg.expired); - break; - case 'syncCompleted': - if (msg.successfully) { - setTimeout(async () => await this.main.refreshBadgeAndMenu(), 2000); - } - break; - case 'openPopup': - await this.main.openPopup(); - break; - case 'promptForLogin': - await BrowserApi.createNewTab('popup/index.html?uilocation=popout', true, true); - break; - case 'showDialogResolve': - this.platformUtilsService.resolveDialogPromise(msg.dialogId, msg.confirmed); - break; - case 'bgCollectPageDetails': - await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId); - break; - case 'bgUpdateContextMenu': - case 'editedCipher': - case 'addedCipher': - case 'deletedCipher': - await this.main.refreshBadgeAndMenu(); - break; - case 'bgReseedStorage': - await this.main.reseedStorage(); - break; - case 'collectPageDetailsResponse': - switch (msg.sender) { - case 'autofiller': - case 'autofill_cmd': - const totpCode = await this.autofillService.doAutoFillActiveTab([{ - frameId: sender.frameId, - tab: msg.tab, - details: msg.details, - }], msg.sender === 'autofill_cmd'); - if (totpCode != null) { - this.platformUtilsService.copyToClipboard(totpCode, { window: window }); - } - break; - case 'contextMenu': - clearTimeout(this.autofillTimeout); - this.pageDetailsToAutoFill.push({ - frameId: sender.frameId, - tab: msg.tab, - details: msg.details, - }); - this.autofillTimeout = setTimeout(async () => await this.autofillPage(), 300); - break; - default: - break; - } - break; - case 'authResult': - const vaultUrl = this.environmentService.getWebVaultUrl(); - - if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) { - return; - } - - try { - BrowserApi.createNewTab('popup/index.html?uilocation=popout#/sso?code=' + - encodeURIComponent(msg.code) + '&state=' + encodeURIComponent(msg.state)); - } - catch { - this.logService.error('Unable to open sso popout tab'); - } - break; - case 'webAuthnResult': - const vaultUrl2 = this.environmentService.getWebVaultUrl(); - - if (msg.referrer == null || Utils.getHostname(vaultUrl2) !== msg.referrer) { - return; - } - - const params = `webAuthnResponse=${encodeURIComponent(msg.data)};` + - `remember=${encodeURIComponent(msg.remember)}`; - BrowserApi.createNewTab(`popup/index.html?uilocation=popout#/2fa;${params}`, undefined, false); - break; - case 'reloadPopup': - this.messagingService.send('reloadPopup'); - break; - case 'emailVerificationRequired': - this.messagingService.send('showDialog', { - dialogId: 'emailVerificationRequired', - title: this.i18nService.t('emailVerificationRequired'), - text: this.i18nService.t('emailVerificationRequiredDesc'), - confirmText: this.i18nService.t('ok'), - type: 'info', - }); - break; - case 'getClickedElementResponse': - this.platformUtilsService.copyToClipboard(msg.identifier, { window: window }); - default: - break; + if (item) { + await BrowserApi.tabSendMessageData( + item.commandToRetry.sender.tab, + "unlockCompleted", + item + ); } - } - - private async autofillPage() { - const totpCode = await this.autofillService.doAutoFill({ - cipher: this.main.loginToAutoFill, - pageDetails: this.pageDetailsToAutoFill, - fillNewPassword: true, - }); - - if (totpCode != null) { - this.platformUtilsService.copyToClipboard(totpCode, { window: window }); + break; + case "addToLockedVaultPendingNotifications": + this.lockedVaultPendingNotifications.push(msg.data); + break; + case "logout": + await this.main.logout(msg.expired); + break; + case "syncCompleted": + if (msg.successfully) { + setTimeout(async () => await this.main.refreshBadgeAndMenu(), 2000); } - - // reset - this.main.loginToAutoFill = null; - this.pageDetailsToAutoFill = []; - } - - private async checkOnInstalled() { - setTimeout(async () => { - if (this.onInstalledReason != null) { - if (this.onInstalledReason === 'install') { - BrowserApi.createNewTab('https://bitwarden.com/browser-start/'); - await this.setDefaultSettings(); - } - - this.onInstalledReason = null; + break; + case "openPopup": + await this.main.openPopup(); + break; + case "promptForLogin": + await BrowserApi.createNewTab("popup/index.html?uilocation=popout", true, true); + break; + case "showDialogResolve": + this.platformUtilsService.resolveDialogPromise(msg.dialogId, msg.confirmed); + break; + case "bgCollectPageDetails": + await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId); + break; + case "bgUpdateContextMenu": + case "editedCipher": + case "addedCipher": + case "deletedCipher": + await this.main.refreshBadgeAndMenu(); + break; + case "bgReseedStorage": + await this.main.reseedStorage(); + break; + case "collectPageDetailsResponse": + switch (msg.sender) { + case "autofiller": + case "autofill_cmd": + const totpCode = await this.autofillService.doAutoFillActiveTab( + [ + { + frameId: sender.frameId, + tab: msg.tab, + details: msg.details, + }, + ], + msg.sender === "autofill_cmd" + ); + if (totpCode != null) { + this.platformUtilsService.copyToClipboard(totpCode, { window: window }); } - }, 100); - } + break; + case "contextMenu": + clearTimeout(this.autofillTimeout); + this.pageDetailsToAutoFill.push({ + frameId: sender.frameId, + tab: msg.tab, + details: msg.details, + }); + this.autofillTimeout = setTimeout(async () => await this.autofillPage(), 300); + break; + default: + break; + } + break; + case "authResult": + const vaultUrl = this.environmentService.getWebVaultUrl(); - private async setDefaultSettings() { - // Default timeout option to "on restart". - const currentVaultTimeout = await this.stateService.getVaultTimeout(); - if (currentVaultTimeout == null) { - await this.stateService.setVaultTimeout(-1); + if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) { + return; } - // Default action to "lock". - const currentVaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); - if (currentVaultTimeoutAction == null) { - await this.stateService.setVaultTimeoutAction('lock'); + try { + BrowserApi.createNewTab( + "popup/index.html?uilocation=popout#/sso?code=" + + encodeURIComponent(msg.code) + + "&state=" + + encodeURIComponent(msg.state) + ); + } catch { + this.logService.error("Unable to open sso popout tab"); } + break; + case "webAuthnResult": + const vaultUrl2 = this.environmentService.getWebVaultUrl(); + + if (msg.referrer == null || Utils.getHostname(vaultUrl2) !== msg.referrer) { + return; + } + + const params = + `webAuthnResponse=${encodeURIComponent(msg.data)};` + + `remember=${encodeURIComponent(msg.remember)}`; + BrowserApi.createNewTab( + `popup/index.html?uilocation=popout#/2fa;${params}`, + undefined, + false + ); + break; + case "reloadPopup": + this.messagingService.send("reloadPopup"); + break; + case "emailVerificationRequired": + this.messagingService.send("showDialog", { + dialogId: "emailVerificationRequired", + title: this.i18nService.t("emailVerificationRequired"), + text: this.i18nService.t("emailVerificationRequiredDesc"), + confirmText: this.i18nService.t("ok"), + type: "info", + }); + break; + case "getClickedElementResponse": + this.platformUtilsService.copyToClipboard(msg.identifier, { window: window }); + default: + break; } + } + + private async autofillPage() { + const totpCode = await this.autofillService.doAutoFill({ + cipher: this.main.loginToAutoFill, + pageDetails: this.pageDetailsToAutoFill, + fillNewPassword: true, + }); + + if (totpCode != null) { + this.platformUtilsService.copyToClipboard(totpCode, { window: window }); + } + + // reset + this.main.loginToAutoFill = null; + this.pageDetailsToAutoFill = []; + } + + private async checkOnInstalled() { + setTimeout(async () => { + if (this.onInstalledReason != null) { + if (this.onInstalledReason === "install") { + BrowserApi.createNewTab("https://bitwarden.com/browser-start/"); + await this.setDefaultSettings(); + } + + this.onInstalledReason = null; + } + }, 100); + } + + private async setDefaultSettings() { + // Default timeout option to "on restart". + const currentVaultTimeout = await this.stateService.getVaultTimeout(); + if (currentVaultTimeout == null) { + await this.stateService.setVaultTimeout(-1); + } + + // Default action to "lock". + const currentVaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); + if (currentVaultTimeoutAction == null) { + await this.stateService.setVaultTimeoutAction("lock"); + } + } } diff --git a/src/models/account.ts b/src/models/account.ts index 9bbbe19436..12bc5eaafe 100644 --- a/src/models/account.ts +++ b/src/models/account.ts @@ -1,24 +1,20 @@ -import { Account as BaseAccount } from 'jslib-common/models/domain/account'; +import { Account as BaseAccount } from "jslib-common/models/domain/account"; -import { BrowserComponentState } from './browserComponentState'; -import { BrowserGroupingsComponentState } from './browserGroupingsComponentState'; -import { BrowserSendComponentState } from './browserSendComponentState'; +import { BrowserComponentState } from "./browserComponentState"; +import { BrowserGroupingsComponentState } from "./browserGroupingsComponentState"; +import { BrowserSendComponentState } from "./browserSendComponentState"; export class Account extends BaseAccount { - groupings?: BrowserGroupingsComponentState; - send?: BrowserSendComponentState; - ciphers?: BrowserComponentState; - sendType?: BrowserComponentState; + groupings?: BrowserGroupingsComponentState; + send?: BrowserSendComponentState; + ciphers?: BrowserComponentState; + sendType?: BrowserComponentState; - constructor(init: Partial) { - super(init); - this.groupings = init.groupings ?? - new BrowserGroupingsComponentState(); - this.send = init.send ?? - new BrowserSendComponentState(); - this.ciphers = init.ciphers ?? - new BrowserComponentState(); - this.sendType = init.sendType ?? - new BrowserComponentState(); - } + constructor(init: Partial) { + super(init); + this.groupings = init.groupings ?? new BrowserGroupingsComponentState(); + this.send = init.send ?? new BrowserSendComponentState(); + this.ciphers = init.ciphers ?? new BrowserComponentState(); + this.sendType = init.sendType ?? new BrowserComponentState(); + } } diff --git a/src/models/browserComponentState.ts b/src/models/browserComponentState.ts index 7fdce0570c..d968726c41 100644 --- a/src/models/browserComponentState.ts +++ b/src/models/browserComponentState.ts @@ -1,4 +1,4 @@ export class BrowserComponentState { - scrollY: number; - searchText: string; + scrollY: number; + searchText: string; } diff --git a/src/models/browserGroupingsComponentState.ts b/src/models/browserGroupingsComponentState.ts index 1dbb797368..40e198a398 100644 --- a/src/models/browserGroupingsComponentState.ts +++ b/src/models/browserGroupingsComponentState.ts @@ -1,15 +1,15 @@ -import { CipherType } from 'jslib-common/enums/cipherType'; -import { CipherView } from 'jslib-common/models/view/cipherView'; -import { CollectionView } from 'jslib-common/models/view/collectionView'; -import { FolderView } from 'jslib-common/models/view/folderView'; -import { BrowserComponentState } from './browserComponentState'; +import { CipherType } from "jslib-common/enums/cipherType"; +import { CipherView } from "jslib-common/models/view/cipherView"; +import { CollectionView } from "jslib-common/models/view/collectionView"; +import { FolderView } from "jslib-common/models/view/folderView"; +import { BrowserComponentState } from "./browserComponentState"; export class BrowserGroupingsComponentState extends BrowserComponentState { - favoriteCiphers: CipherView[]; - noFolderCiphers: CipherView[]; - collectionCounts: Map; - typeCounts: Map; - folders: FolderView[]; - collections: CollectionView[]; - deletedCount: number; + favoriteCiphers: CipherView[]; + noFolderCiphers: CipherView[]; + collectionCounts: Map; + typeCounts: Map; + folders: FolderView[]; + collections: CollectionView[]; + deletedCount: number; } diff --git a/src/models/browserSendComponentState.ts b/src/models/browserSendComponentState.ts index cbecb4807d..5c2fb54a17 100644 --- a/src/models/browserSendComponentState.ts +++ b/src/models/browserSendComponentState.ts @@ -1,8 +1,8 @@ -import { SendType } from 'jslib-common/enums/sendType'; -import { SendView } from 'jslib-common/models/view/sendView'; -import { BrowserComponentState } from './browserComponentState'; +import { SendType } from "jslib-common/enums/sendType"; +import { SendView } from "jslib-common/models/view/sendView"; +import { BrowserComponentState } from "./browserComponentState"; export class BrowserSendComponentState extends BrowserComponentState { - sends: SendView[]; - typeCounts: Map; + sends: SendView[]; + typeCounts: Map; } diff --git a/src/popup/accounts/home.component.ts b/src/popup/accounts/home.component.ts index c3452e5080..4b13f408b4 100644 --- a/src/popup/accounts/home.component.ts +++ b/src/popup/accounts/home.component.ts @@ -1,51 +1,64 @@ -import { Component } from '@angular/core'; +import { Component } from "@angular/core"; -import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StateService } from '../../services/abstractions/state.service'; +import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "../../services/abstractions/state.service"; -import { Utils } from 'jslib-common/misc/utils'; +import { Utils } from "jslib-common/misc/utils"; @Component({ - selector: 'app-home', - templateUrl: 'home.component.html', + selector: "app-home", + templateUrl: "home.component.html", }) export class HomeComponent { - constructor(protected platformUtilsService: PlatformUtilsService, - private passwordGenerationService: PasswordGenerationService, private stateService: StateService, - private cryptoFunctionService: CryptoFunctionService, private environmentService: EnvironmentService) { } + constructor( + protected platformUtilsService: PlatformUtilsService, + private passwordGenerationService: PasswordGenerationService, + private stateService: StateService, + private cryptoFunctionService: CryptoFunctionService, + private environmentService: EnvironmentService + ) {} - async launchSsoBrowser() { - // Generate necessary sso params - const passwordOptions: any = { - type: 'password', - length: 64, - uppercase: true, - lowercase: true, - numbers: true, - special: false, - }; + async launchSsoBrowser() { + // Generate necessary sso params + const passwordOptions: any = { + type: "password", + length: 64, + uppercase: true, + lowercase: true, + numbers: true, + special: false, + }; - const state = (await this.passwordGenerationService.generatePassword(passwordOptions)) + ':clientId=browser'; - const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); - const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, 'sha256'); - const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); + const state = + (await this.passwordGenerationService.generatePassword(passwordOptions)) + + ":clientId=browser"; + const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); + const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256"); + const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); - await this.stateService.setSsoCodeVerifier(codeVerifier); - await this.stateService.setSsoState(state); + await this.stateService.setSsoCodeVerifier(codeVerifier); + await this.stateService.setSsoState(state); - let url = this.environmentService.getWebVaultUrl(); - if (url == null) { - url = 'https://vault.bitwarden.com'; - } - - const redirectUri = url + '/sso-connector.html'; - - // Launch browser - this.platformUtilsService.launchUri(url + '/#/sso?clientId=browser' + - '&redirectUri=' + encodeURIComponent(redirectUri) + - '&state=' + state + '&codeChallenge=' + codeChallenge); + let url = this.environmentService.getWebVaultUrl(); + if (url == null) { + url = "https://vault.bitwarden.com"; } + + const redirectUri = url + "/sso-connector.html"; + + // Launch browser + this.platformUtilsService.launchUri( + url + + "/#/sso?clientId=browser" + + "&redirectUri=" + + encodeURIComponent(redirectUri) + + "&state=" + + state + + "&codeChallenge=" + + codeChallenge + ); + } } diff --git a/src/popup/accounts/lock.component.ts b/src/popup/accounts/lock.component.ts index 1506fcd011..59ad84cb96 100644 --- a/src/popup/accounts/lock.component.ts +++ b/src/popup/accounts/lock.component.ts @@ -1,77 +1,99 @@ -import { Component, NgZone } from '@angular/core'; -import { Router } from '@angular/router'; -import Swal from 'sweetalert2'; +import { Component, NgZone } from "@angular/core"; +import { Router } from "@angular/router"; +import Swal from "sweetalert2"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; -import { StateService } from '../../services/abstractions/state.service'; +import { StateService } from "../../services/abstractions/state.service"; -import { LockComponent as BaseLockComponent } from 'jslib-angular/components/lock.component'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { LockComponent as BaseLockComponent } from "jslib-angular/components/lock.component"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; @Component({ - selector: 'app-lock', - templateUrl: 'lock.component.html', + selector: "app-lock", + templateUrl: "lock.component.html", }) export class LockComponent extends BaseLockComponent { - private isInitialLockScreen: boolean; + private isInitialLockScreen: boolean; - constructor(router: Router, i18nService: I18nService, - platformUtilsService: PlatformUtilsService, messagingService: MessagingService, cryptoService: CryptoService, - vaultTimeoutService: VaultTimeoutService, environmentService: EnvironmentService, - stateService: StateService, apiService: ApiService, logService: LogService, keyConnectorService: KeyConnectorService, ngZone: NgZone) { - super(router, i18nService, platformUtilsService, - messagingService, cryptoService, vaultTimeoutService, - environmentService, stateService, apiService, logService, keyConnectorService, ngZone); - this.successRoute = '/tabs/current'; - this.isInitialLockScreen = (window as any).previousPopupUrl == null; - } + constructor( + router: Router, + i18nService: I18nService, + platformUtilsService: PlatformUtilsService, + messagingService: MessagingService, + cryptoService: CryptoService, + vaultTimeoutService: VaultTimeoutService, + environmentService: EnvironmentService, + stateService: StateService, + apiService: ApiService, + logService: LogService, + keyConnectorService: KeyConnectorService, + ngZone: NgZone + ) { + super( + router, + i18nService, + platformUtilsService, + messagingService, + cryptoService, + vaultTimeoutService, + environmentService, + stateService, + apiService, + logService, + keyConnectorService, + ngZone + ); + this.successRoute = "/tabs/current"; + this.isInitialLockScreen = (window as any).previousPopupUrl == null; + } - async ngOnInit() { - await super.ngOnInit(); - const disableAutoBiometricsPrompt = await this.stateService.getDisableAutoBiometricsPrompt() ?? true; + async ngOnInit() { + await super.ngOnInit(); + const disableAutoBiometricsPrompt = + (await this.stateService.getDisableAutoBiometricsPrompt()) ?? true; - window.setTimeout(async () => { - document.getElementById(this.pinLock ? 'pin' : 'masterPassword').focus(); - if (this.biometricLock && !disableAutoBiometricsPrompt && this.isInitialLockScreen) { - if (await this.vaultTimeoutService.isLocked()) { - await this.unlockBiometric(); - } - } - }, 100); - } - - async unlockBiometric(): Promise { - if (!this.biometricLock) { - return; + window.setTimeout(async () => { + document.getElementById(this.pinLock ? "pin" : "masterPassword").focus(); + if (this.biometricLock && !disableAutoBiometricsPrompt && this.isInitialLockScreen) { + if (await this.vaultTimeoutService.isLocked()) { + await this.unlockBiometric(); } + } + }, 100); + } - const div = document.createElement('div'); - div.innerHTML = `
${this.i18nService.t('awaitDesktop')}
`; - - Swal.fire({ - heightAuto: false, - buttonsStyling: false, - html: div, - showCancelButton: true, - cancelButtonText: this.i18nService.t('cancel'), - showConfirmButton: false, - }); - - const success = await super.unlockBiometric(); - - // Avoid closing the error dialogs - if (success) { - Swal.close(); - } - - return success; + async unlockBiometric(): Promise { + if (!this.biometricLock) { + return; } + + const div = document.createElement("div"); + div.innerHTML = `
${this.i18nService.t("awaitDesktop")}
`; + + Swal.fire({ + heightAuto: false, + buttonsStyling: false, + html: div, + showCancelButton: true, + cancelButtonText: this.i18nService.t("cancel"), + showConfirmButton: false, + }); + + const success = await super.unlockBiometric(); + + // Avoid closing the error dialogs + if (success) { + Swal.close(); + } + + return success; + } } diff --git a/src/popup/accounts/login.component.ts b/src/popup/accounts/login.component.ts index 276d898437..0cdb238c19 100644 --- a/src/popup/accounts/login.component.ts +++ b/src/popup/accounts/login.component.ts @@ -1,45 +1,61 @@ -import { Component, NgZone } from '@angular/core'; -import { Router } from '@angular/router'; +import { Component, NgZone } from "@angular/core"; +import { Router } from "@angular/router"; -import { AuthService } from 'jslib-common/abstractions/auth.service'; -import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StateService } from 'jslib-common/abstractions/state.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { SyncService } from 'jslib-common/abstractions/sync.service'; +import { AuthService } from "jslib-common/abstractions/auth.service"; +import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { StorageService } from "jslib-common/abstractions/storage.service"; +import { SyncService } from "jslib-common/abstractions/sync.service"; -import { LoginComponent as BaseLoginComponent } from 'jslib-angular/components/login.component'; +import { LoginComponent as BaseLoginComponent } from "jslib-angular/components/login.component"; @Component({ - selector: 'app-login', - templateUrl: 'login.component.html', + selector: "app-login", + templateUrl: "login.component.html", }) export class LoginComponent extends BaseLoginComponent { - constructor(authService: AuthService, router: Router, - protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, - protected stateService: StateService, protected environmentService: EnvironmentService, - protected passwordGenerationService: PasswordGenerationService, - protected cryptoFunctionService: CryptoFunctionService, logService: LogService, - syncService: SyncService, ngZone: NgZone) { - super(authService, router, platformUtilsService, i18nService, - stateService, environmentService, passwordGenerationService, cryptoFunctionService, - logService, ngZone); - super.onSuccessfulLogin = async () => { - await syncService.fullSync(true); - }; - super.successRoute = '/tabs/vault'; - } + constructor( + authService: AuthService, + router: Router, + protected platformUtilsService: PlatformUtilsService, + protected i18nService: I18nService, + protected stateService: StateService, + protected environmentService: EnvironmentService, + protected passwordGenerationService: PasswordGenerationService, + protected cryptoFunctionService: CryptoFunctionService, + logService: LogService, + syncService: SyncService, + ngZone: NgZone + ) { + super( + authService, + router, + platformUtilsService, + i18nService, + stateService, + environmentService, + passwordGenerationService, + cryptoFunctionService, + logService, + ngZone + ); + super.onSuccessfulLogin = async () => { + await syncService.fullSync(true); + }; + super.successRoute = "/tabs/vault"; + } - settings() { - this.router.navigate(['environment']); - } + settings() { + this.router.navigate(["environment"]); + } - async submit() { - await super.submit(); - this.stateService.setRememberedEmail(this.email); - } + async submit() { + await super.submit(); + this.stateService.setRememberedEmail(this.email); + } } diff --git a/src/popup/accounts/set-password.component.ts b/src/popup/accounts/set-password.component.ts index ba124cb14f..6c42f956ec 100644 --- a/src/popup/accounts/set-password.component.ts +++ b/src/popup/accounts/set-password.component.ts @@ -1,65 +1,79 @@ -import { Component } from '@angular/core'; +import { Component } from "@angular/core"; -import { - ActivatedRoute, - Router, -} from '@angular/router'; +import { ActivatedRoute, Router } from "@angular/router"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService } from 'jslib-common/abstractions/policy.service'; -import { StateService } from 'jslib-common/abstractions/state.service'; -import { SyncService } from 'jslib-common/abstractions/sync.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService } from "jslib-common/abstractions/policy.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { SyncService } from "jslib-common/abstractions/sync.service"; -import { - SetPasswordComponent as BaseSetPasswordComponent, -} from 'jslib-angular/components/set-password.component'; +import { SetPasswordComponent as BaseSetPasswordComponent } from "jslib-angular/components/set-password.component"; @Component({ - selector: 'app-set-password', - templateUrl: 'set-password.component.html', + selector: "app-set-password", + templateUrl: "set-password.component.html", }) export class SetPasswordComponent extends BaseSetPasswordComponent { - constructor(apiService: ApiService, i18nService: I18nService, - cryptoService: CryptoService, messagingService: MessagingService, - stateService: StateService, passwordGenerationService: PasswordGenerationService, - platformUtilsService: PlatformUtilsService, policyService: PolicyService, router: Router, - syncService: SyncService, route: ActivatedRoute) { - super(i18nService, cryptoService, messagingService, passwordGenerationService, - platformUtilsService, policyService, router, apiService, syncService, route, stateService); - } + constructor( + apiService: ApiService, + i18nService: I18nService, + cryptoService: CryptoService, + messagingService: MessagingService, + stateService: StateService, + passwordGenerationService: PasswordGenerationService, + platformUtilsService: PlatformUtilsService, + policyService: PolicyService, + router: Router, + syncService: SyncService, + route: ActivatedRoute + ) { + super( + i18nService, + cryptoService, + messagingService, + passwordGenerationService, + platformUtilsService, + policyService, + router, + apiService, + syncService, + route, + stateService + ); + } - get masterPasswordScoreWidth() { - return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20; - } + get masterPasswordScoreWidth() { + return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20; + } - get masterPasswordScoreColor() { - switch (this.masterPasswordScore) { - case 4: - return 'success'; - case 3: - return 'primary'; - case 2: - return 'warning'; - default: - return 'danger'; - } + get masterPasswordScoreColor() { + switch (this.masterPasswordScore) { + case 4: + return "success"; + case 3: + return "primary"; + case 2: + return "warning"; + default: + return "danger"; } + } - get masterPasswordScoreText() { - switch (this.masterPasswordScore) { - case 4: - return this.i18nService.t('strong'); - case 3: - return this.i18nService.t('good'); - case 2: - return this.i18nService.t('weak'); - default: - return this.masterPasswordScore != null ? this.i18nService.t('weak') : null; - } + get masterPasswordScoreText() { + switch (this.masterPasswordScore) { + case 4: + return this.i18nService.t("strong"); + case 3: + return this.i18nService.t("good"); + case 2: + return this.i18nService.t("weak"); + default: + return this.masterPasswordScore != null ? this.i18nService.t("weak") : null; } + } } diff --git a/src/popup/accounts/sso.component.ts b/src/popup/accounts/sso.component.ts index 007999b5aa..9777bfcced 100644 --- a/src/popup/accounts/sso.component.ts +++ b/src/popup/accounts/sso.component.ts @@ -1,54 +1,71 @@ -import { Component } from '@angular/core'; +import { Component } from "@angular/core"; -import { - ActivatedRoute, - Router, -} from '@angular/router'; +import { ActivatedRoute, Router } from "@angular/router"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { AuthService } from 'jslib-common/abstractions/auth.service'; -import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { SyncService } from 'jslib-common/abstractions/sync.service'; -import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { AuthService } from "jslib-common/abstractions/auth.service"; +import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { SyncService } from "jslib-common/abstractions/sync.service"; +import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; -import { SsoComponent as BaseSsoComponent } from 'jslib-angular/components/sso.component'; -import { BrowserApi } from '../../browser/browserApi'; +import { SsoComponent as BaseSsoComponent } from "jslib-angular/components/sso.component"; +import { BrowserApi } from "../../browser/browserApi"; -import { StateService } from '../../services/abstractions/state.service'; +import { StateService } from "../../services/abstractions/state.service"; @Component({ - selector: 'app-sso', - templateUrl: 'sso.component.html', + selector: "app-sso", + templateUrl: "sso.component.html", }) export class SsoComponent extends BaseSsoComponent { - constructor(authService: AuthService, router: Router, - i18nService: I18nService, route: ActivatedRoute, stateService: StateService, - platformUtilsService: PlatformUtilsService, apiService: ApiService, - cryptoFunctionService: CryptoFunctionService, passwordGenerationService: PasswordGenerationService, - syncService: SyncService, environmentService: EnvironmentService, logService: LogService, - private vaultTimeoutService: VaultTimeoutService) { - super(authService, router, i18nService, route, stateService, platformUtilsService, - apiService, cryptoFunctionService, environmentService, passwordGenerationService, logService); + constructor( + authService: AuthService, + router: Router, + i18nService: I18nService, + route: ActivatedRoute, + stateService: StateService, + platformUtilsService: PlatformUtilsService, + apiService: ApiService, + cryptoFunctionService: CryptoFunctionService, + passwordGenerationService: PasswordGenerationService, + syncService: SyncService, + environmentService: EnvironmentService, + logService: LogService, + private vaultTimeoutService: VaultTimeoutService + ) { + super( + authService, + router, + i18nService, + route, + stateService, + platformUtilsService, + apiService, + cryptoFunctionService, + environmentService, + passwordGenerationService, + logService + ); - const url = this.environmentService.getWebVaultUrl(); + const url = this.environmentService.getWebVaultUrl(); - this.redirectUri = url + '/sso-connector.html'; - this.clientId = 'browser'; + this.redirectUri = url + "/sso-connector.html"; + this.clientId = "browser"; - super.onSuccessfulLogin = async () => { - await syncService.fullSync(true); - if (await this.vaultTimeoutService.isLocked()) { - // If the vault is unlocked then this will clear keys from memory, which we don't want to do - BrowserApi.reloadOpenWindows(); - } + super.onSuccessfulLogin = async () => { + await syncService.fullSync(true); + if (await this.vaultTimeoutService.isLocked()) { + // If the vault is unlocked then this will clear keys from memory, which we don't want to do + BrowserApi.reloadOpenWindows(); + } - const thisWindow = window.open('', '_self'); - thisWindow.close(); - }; - } + const thisWindow = window.open("", "_self"); + thisWindow.close(); + }; + } } diff --git a/src/popup/accounts/two-factor.component.ts b/src/popup/accounts/two-factor.component.ts index a7f18bffa0..abfb60367c 100644 --- a/src/popup/accounts/two-factor.component.ts +++ b/src/popup/accounts/two-factor.component.ts @@ -1,115 +1,139 @@ -import { ChangeDetectorRef, Component, NgZone } from '@angular/core'; -import { - ActivatedRoute, - Router, -} from '@angular/router'; -import { first } from 'rxjs/operators'; +import { ChangeDetectorRef, Component, NgZone } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { first } from "rxjs/operators"; -import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType'; +import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { AuthService } from 'jslib-common/abstractions/auth.service'; -import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { SyncService } from 'jslib-common/abstractions/sync.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { AuthService } from "jslib-common/abstractions/auth.service"; +import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { SyncService } from "jslib-common/abstractions/sync.service"; -import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib-angular/components/two-factor.component'; +import { TwoFactorComponent as BaseTwoFactorComponent } from "jslib-angular/components/two-factor.component"; -import { PopupUtilsService } from '../services/popup-utils.service'; +import { PopupUtilsService } from "../services/popup-utils.service"; -import { StateService } from 'jslib-common/abstractions/state.service'; -import { BrowserApi } from '../../browser/browserApi'; +import { StateService } from "jslib-common/abstractions/state.service"; +import { BrowserApi } from "../../browser/browserApi"; -const BroadcasterSubscriptionId = 'TwoFactorComponent'; +const BroadcasterSubscriptionId = "TwoFactorComponent"; @Component({ - selector: 'app-two-factor', - templateUrl: 'two-factor.component.html', + selector: "app-two-factor", + templateUrl: "two-factor.component.html", }) export class TwoFactorComponent extends BaseTwoFactorComponent { - showNewWindowMessage = false; + showNewWindowMessage = false; - constructor(authService: AuthService, router: Router, - i18nService: I18nService, apiService: ApiService, - platformUtilsService: PlatformUtilsService, private syncService: SyncService, - environmentService: EnvironmentService, private ngZone: NgZone, - private broadcasterService: BroadcasterService, private changeDetectorRef: ChangeDetectorRef, - private popupUtilsService: PopupUtilsService, stateService: StateService, route: ActivatedRoute, - private messagingService: MessagingService, logService: LogService) { - super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService, - stateService, route, logService); + constructor( + authService: AuthService, + router: Router, + i18nService: I18nService, + apiService: ApiService, + platformUtilsService: PlatformUtilsService, + private syncService: SyncService, + environmentService: EnvironmentService, + private ngZone: NgZone, + private broadcasterService: BroadcasterService, + private changeDetectorRef: ChangeDetectorRef, + private popupUtilsService: PopupUtilsService, + stateService: StateService, + route: ActivatedRoute, + private messagingService: MessagingService, + logService: LogService + ) { + super( + authService, + router, + i18nService, + apiService, + platformUtilsService, + window, + environmentService, + stateService, + route, + logService + ); + super.onSuccessfulLogin = () => { + return syncService.fullSync(true); + }; + super.successRoute = "/tabs/vault"; + this.webAuthnNewTab = + this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari(); + } + + async ngOnInit() { + if (this.route.snapshot.paramMap.has("webAuthnResponse")) { + // WebAuthn fallback response + this.selectedProviderType = TwoFactorProviderType.WebAuthn; + this.token = this.route.snapshot.paramMap.get("webAuthnResponse"); + super.onSuccessfulLogin = async () => { + this.syncService.fullSync(true); + this.messagingService.send("reloadPopup"); + window.close(); + }; + this.remember = this.route.snapshot.paramMap.get("remember") === "true"; + await this.doSubmit(); + return; + } + + await super.ngOnInit(); + if (this.selectedProviderType == null) { + return; + } + + // WebAuthn prompt appears inside the popup on linux, and requires a larger popup width + // than usual to avoid cutting off the dialog. + if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && (await this.isLinux())) { + document.body.classList.add("linux-webauthn"); + } + + if ( + this.selectedProviderType === TwoFactorProviderType.Email && + this.popupUtilsService.inPopup(window) + ) { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("popup2faCloseMessage"), + null, + this.i18nService.t("yes"), + this.i18nService.t("no") + ); + if (confirmed) { + this.popupUtilsService.popOut(window); + } + } + + this.route.queryParams.pipe(first()).subscribe(async (qParams) => { + if (qParams.sso === "true") { super.onSuccessfulLogin = () => { - return syncService.fullSync(true); + BrowserApi.reloadOpenWindows(); + const thisWindow = window.open("", "_self"); + thisWindow.close(); + return this.syncService.fullSync(true); }; - super.successRoute = '/tabs/vault'; - this.webAuthnNewTab = this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari(); + } + }); + } + + async ngOnDestroy() { + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + + if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && (await this.isLinux())) { + document.body.classList.remove("linux-webauthn"); } + super.ngOnDestroy(); + } - async ngOnInit() { - if (this.route.snapshot.paramMap.has('webAuthnResponse')) { - // WebAuthn fallback response - this.selectedProviderType = TwoFactorProviderType.WebAuthn; - this.token = this.route.snapshot.paramMap.get('webAuthnResponse'); - super.onSuccessfulLogin = async () => { - this.syncService.fullSync(true); - this.messagingService.send('reloadPopup'); - window.close(); - }; - this.remember = this.route.snapshot.paramMap.get('remember') === 'true'; - await this.doSubmit(); - return; - } + anotherMethod() { + this.router.navigate(["2fa-options"]); + } - await super.ngOnInit(); - if (this.selectedProviderType == null) { - return; - } - - // WebAuthn prompt appears inside the popup on linux, and requires a larger popup width - // than usual to avoid cutting off the dialog. - if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && await this.isLinux()) { - document.body.classList.add('linux-webauthn'); - } - - if (this.selectedProviderType === TwoFactorProviderType.Email && - this.popupUtilsService.inPopup(window)) { - const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('popup2faCloseMessage'), - null, this.i18nService.t('yes'), this.i18nService.t('no')); - if (confirmed) { - this.popupUtilsService.popOut(window); - } - } - - this.route.queryParams.pipe(first()).subscribe(async qParams => { - if (qParams.sso === 'true') { - super.onSuccessfulLogin = () => { - BrowserApi.reloadOpenWindows(); - const thisWindow = window.open('', '_self'); - thisWindow.close(); - return this.syncService.fullSync(true); - }; - } - }); - } - - async ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - - if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && await this.isLinux()) { - document.body.classList.remove('linux-webauthn'); - } - super.ngOnDestroy(); - } - - anotherMethod() { - this.router.navigate(['2fa-options']); - } - - async isLinux() { - return (await BrowserApi.getPlatformInfo()).os === 'linux'; - } + async isLinux() { + return (await BrowserApi.getPlatformInfo()).os === "linux"; + } } diff --git a/src/popup/accounts/update-temp-password.component.ts b/src/popup/accounts/update-temp-password.component.ts index 2d01dddba9..647351c306 100644 --- a/src/popup/accounts/update-temp-password.component.ts +++ b/src/popup/accounts/update-temp-password.component.ts @@ -1,66 +1,82 @@ -import { Component } from '@angular/core'; +import { Component } from "@angular/core"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService } from 'jslib-common/abstractions/policy.service'; -import { SyncService } from 'jslib-common/abstractions/sync.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService } from "jslib-common/abstractions/policy.service"; +import { SyncService } from "jslib-common/abstractions/sync.service"; -import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from 'jslib-angular/components/update-temp-password.component'; -import { StateService } from '../../services/abstractions/state.service'; +import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "jslib-angular/components/update-temp-password.component"; +import { StateService } from "../../services/abstractions/state.service"; interface MasterPasswordScore { - Color: string; - Text: string; - Width: number; + Color: string; + Text: string; + Width: number; } @Component({ - selector: 'app-update-temp-password', - templateUrl: 'update-temp-password.component.html', + selector: "app-update-temp-password", + templateUrl: "update-temp-password.component.html", }) export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent { - get masterPasswordScoreStyle(): MasterPasswordScore { - const scoreWidth = this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20; - switch (this.masterPasswordScore) { - case 4: - return { - Color: 'bg-success', - Text: 'strong', - Width: scoreWidth, - }; - case 3: - return { - Color: 'bg-primary', - Text: 'good', - Width: scoreWidth, - }; - case 2: - return { - Color: 'bg-warning', - Text: 'weak', - Width: scoreWidth, - }; - default: - return { - Color: 'bg-danger', - Text: 'weak', - Width: scoreWidth, - }; - } + get masterPasswordScoreStyle(): MasterPasswordScore { + const scoreWidth = this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20; + switch (this.masterPasswordScore) { + case 4: + return { + Color: "bg-success", + Text: "strong", + Width: scoreWidth, + }; + case 3: + return { + Color: "bg-primary", + Text: "good", + Width: scoreWidth, + }; + case 2: + return { + Color: "bg-warning", + Text: "weak", + Width: scoreWidth, + }; + default: + return { + Color: "bg-danger", + Text: "weak", + Width: scoreWidth, + }; } + } - constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService, - passwordGenerationService: PasswordGenerationService, policyService: PolicyService, - cryptoService: CryptoService, stateService: StateService, - messagingService: MessagingService, apiService: ApiService, - syncService: SyncService, logService: LogService) { - super(i18nService, platformUtilsService, passwordGenerationService, policyService, - cryptoService, messagingService, apiService, stateService, - syncService, logService); - } + constructor( + i18nService: I18nService, + platformUtilsService: PlatformUtilsService, + passwordGenerationService: PasswordGenerationService, + policyService: PolicyService, + cryptoService: CryptoService, + stateService: StateService, + messagingService: MessagingService, + apiService: ApiService, + syncService: SyncService, + logService: LogService + ) { + super( + i18nService, + platformUtilsService, + passwordGenerationService, + policyService, + cryptoService, + messagingService, + apiService, + stateService, + syncService, + logService + ); + } } diff --git a/src/popup/app.component.ts b/src/popup/app.component.ts index 8ccd14f95f..8658b798ad 100644 --- a/src/popup/app.component.ts +++ b/src/popup/app.component.ts @@ -1,238 +1,249 @@ -import { - ChangeDetectorRef, - Component, - NgZone, - OnInit, - SecurityContext, -} from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; -import { - NavigationEnd, - Router, - RouterOutlet, -} from '@angular/router'; -import { - IndividualConfig, - ToastrService, -} from 'ngx-toastr'; -import Swal, { SweetAlertIcon } from 'sweetalert2/src/sweetalert2.js'; -import { BrowserApi } from '../browser/browserApi'; +import { ChangeDetectorRef, Component, NgZone, OnInit, SecurityContext } from "@angular/core"; +import { DomSanitizer } from "@angular/platform-browser"; +import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; +import { IndividualConfig, ToastrService } from "ngx-toastr"; +import Swal, { SweetAlertIcon } from "sweetalert2/src/sweetalert2.js"; +import { BrowserApi } from "../browser/browserApi"; -import { AuthService } from 'jslib-common/abstractions/auth.service'; -import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; +import { AuthService } from "jslib-common/abstractions/auth.service"; +import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StorageService } from "jslib-common/abstractions/storage.service"; -import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; -import { routerTransition } from './app-routing.animations'; +import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; +import { routerTransition } from "./app-routing.animations"; -import { StateService } from '../services/abstractions/state.service'; +import { StateService } from "../services/abstractions/state.service"; @Component({ - selector: 'app-root', - styles: [], - animations: [routerTransition], - template: ` -
- -
`, + selector: "app-root", + styles: [], + animations: [routerTransition], + template: `
+ +
`, }) export class AppComponent implements OnInit { + private lastActivity: number = null; - private lastActivity: number = null; + constructor( + private toastrService: ToastrService, + private storageService: StorageService, + private broadcasterService: BroadcasterService, + private authService: AuthService, + private i18nService: I18nService, + private router: Router, + private stateService: StateService, + private messagingService: MessagingService, + private changeDetectorRef: ChangeDetectorRef, + private ngZone: NgZone, + private sanitizer: DomSanitizer, + private platformUtilsService: PlatformUtilsService, + private keyConnectoService: KeyConnectorService + ) {} - constructor(private toastrService: ToastrService, private storageService: StorageService, - private broadcasterService: BroadcasterService, private authService: AuthService, - private i18nService: I18nService, private router: Router, - private stateService: StateService, private messagingService: MessagingService, - private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone, - private sanitizer: DomSanitizer, private platformUtilsService: PlatformUtilsService, - private keyConnectoService: KeyConnectorService) { } + ngOnInit() { + if (BrowserApi.getBackgroundPage() == null) { + return; + } - ngOnInit() { - if (BrowserApi.getBackgroundPage() == null) { - return; - } + this.ngZone.runOutsideAngular(() => { + window.onmousedown = () => this.recordActivity(); + window.ontouchstart = () => this.recordActivity(); + window.onclick = () => this.recordActivity(); + window.onscroll = () => this.recordActivity(); + window.onkeypress = () => this.recordActivity(); + }); - this.ngZone.runOutsideAngular(() => { - window.onmousedown = () => this.recordActivity(); - window.ontouchstart = () => this.recordActivity(); - window.onclick = () => this.recordActivity(); - window.onscroll = () => this.recordActivity(); - window.onkeypress = () => this.recordActivity(); + (window as any).bitwardenPopupMainMessageListener = async ( + msg: any, + sender: any, + sendResponse: any + ) => { + if (msg.command === "doneLoggingOut") { + this.ngZone.run(async () => { + this.authService.logOut(async () => { + if (msg.expired) { + this.showToast({ + type: "warning", + title: this.i18nService.t("loggedOut"), + text: this.i18nService.t("loginExpired"), + }); + } + await this.stateService.clean(); + this.router.navigate(["home"]); + }); + this.changeDetectorRef.detectChanges(); }); - - (window as any).bitwardenPopupMainMessageListener = async (msg: any, sender: any, sendResponse: any) => { - if (msg.command === 'doneLoggingOut') { - this.ngZone.run(async () => { - this.authService.logOut(async () => { - if (msg.expired) { - this.showToast({ - type: 'warning', - title: this.i18nService.t('loggedOut'), - text: this.i18nService.t('loginExpired'), - }); - } - await this.stateService.clean(); - this.router.navigate(['home']); - }); - this.changeDetectorRef.detectChanges(); - }); - } else if (msg.command === 'authBlocked') { - this.ngZone.run(() => { - this.router.navigate(['home']); - }); - } else if (msg.command === 'locked') { - this.ngZone.run(() => { - this.router.navigate(['lock']); - }); - } else if (msg.command === 'showDialog') { - await this.showDialog(msg); - } else if (msg.command === 'showToast') { - this.ngZone.run(() => { - this.showToast(msg); - }); - } else if (msg.command === 'reloadProcess') { - const windowReload = this.platformUtilsService.isSafari() || - this.platformUtilsService.isFirefox() || this.platformUtilsService.isOpera(); - if (windowReload) { - // Wait to make sure background has reloaded first. - window.setTimeout(() => BrowserApi.reloadExtension(window), 2000); - } - } else if (msg.command === 'reloadPopup') { - this.ngZone.run(() => { - this.router.navigate(['/']); - }); - } else if (msg.command === 'convertAccountToKeyConnector') { - this.ngZone.run(async () => { - await this.keyConnectoService.setConvertAccountRequired(true); - this.router.navigate(['/remove-password']); - }); - } else { - msg.webExtSender = sender; - this.broadcasterService.send(msg); - } - }; - - BrowserApi.messageListener('app.component', (window as any).bitwardenPopupMainMessageListener); - - this.router.events.subscribe(async event => { - if (event instanceof NavigationEnd) { - const url = event.urlAfterRedirects || event.url || ''; - if (url.startsWith('/tabs/') && (window as any).previousPopupUrl != null && - (window as any).previousPopupUrl.startsWith('/tabs/')) { - await this.stateService.setBrowserGroupingComponentState(null); - await this.stateService.setBrowserCipherComponentState(null); - await this.stateService.setBrowserSendComponentState(null); - await this.stateService.setBrowserSendTypeComponentState(null); - } - if (url.startsWith('/tabs/')) { - await this.stateService.setAddEditCipherInfo(null); - } - (window as any).previousPopupUrl = url; - - // Clear route direction after animation (400ms) - if ((window as any).routeDirection != null) { - window.setTimeout(() => { - (window as any).routeDirection = null; - }, 400); - } - } + } else if (msg.command === "authBlocked") { + this.ngZone.run(() => { + this.router.navigate(["home"]); }); - } - - getState(outlet: RouterOutlet) { - if (outlet.activatedRouteData.state === 'ciphers') { - const routeDirection = (window as any).routeDirection != null ? (window as any).routeDirection : ''; - return 'ciphers_direction=' + routeDirection + '_' + - (outlet.activatedRoute.queryParams as any).value.folderId + '_' + - (outlet.activatedRoute.queryParams as any).value.collectionId; - } else { - return outlet.activatedRouteData.state; - } - } - - private async recordActivity() { - const now = (new Date()).getTime(); - if (this.lastActivity != null && now - this.lastActivity < 250) { - return; - } - - this.lastActivity = now; - await this.stateService.setLastActive(now); - } - - private showToast(msg: any) { - let message = ''; - - const options: Partial = {}; - - if (typeof (msg.text) === 'string') { - message = msg.text; - } else if (msg.text.length === 1) { - message = msg.text[0]; - } else { - msg.text.forEach((t: string) => - message += ('

' + this.sanitizer.sanitize(SecurityContext.HTML, t) + '

')); - options.enableHtml = true; - } - if (msg.options != null) { - if (msg.options.trustedHtml === true) { - options.enableHtml = true; - } - if (msg.options.timeout != null && msg.options.timeout > 0) { - options.timeOut = msg.options.timeout; - } - } - - this.toastrService.show(message, msg.title, options, 'toast-' + msg.type); - } - - private async showDialog(msg: any) { - let iconClasses: string = null; - const type = msg.type; - if (type != null) { - // If you add custom types to this part, the type to SweetAlertIcon cast below needs to be changed. - switch (type) { - case 'success': - iconClasses = 'fa-check text-success'; - break; - case 'warning': - iconClasses = 'fa-warning text-warning'; - break; - case 'error': - iconClasses = 'fa-bolt text-danger'; - break; - case 'info': - iconClasses = 'fa-info-circle text-info'; - break; - default: - break; - } - } - - const cancelText = msg.cancelText; - const confirmText = msg.confirmText; - const confirmed = await Swal.fire({ - heightAuto: false, - buttonsStyling: false, - icon: type as SweetAlertIcon, // required to be any of the SweetAlertIcons to output the iconHtml. - iconHtml: iconClasses != null ? `` : undefined, - text: msg.text, - html: msg.html, - titleText: msg.title, - showCancelButton: (cancelText != null), - cancelButtonText: cancelText, - showConfirmButton: true, - confirmButtonText: confirmText == null ? this.i18nService.t('ok') : confirmText, - timer: 300000, + } else if (msg.command === "locked") { + this.ngZone.run(() => { + this.router.navigate(["lock"]); }); - - this.messagingService.send('showDialogResolve', { - dialogId: msg.dialogId, - confirmed: confirmed.value, + } else if (msg.command === "showDialog") { + await this.showDialog(msg); + } else if (msg.command === "showToast") { + this.ngZone.run(() => { + this.showToast(msg); }); + } else if (msg.command === "reloadProcess") { + const windowReload = + this.platformUtilsService.isSafari() || + this.platformUtilsService.isFirefox() || + this.platformUtilsService.isOpera(); + if (windowReload) { + // Wait to make sure background has reloaded first. + window.setTimeout(() => BrowserApi.reloadExtension(window), 2000); + } + } else if (msg.command === "reloadPopup") { + this.ngZone.run(() => { + this.router.navigate(["/"]); + }); + } else if (msg.command === "convertAccountToKeyConnector") { + this.ngZone.run(async () => { + await this.keyConnectoService.setConvertAccountRequired(true); + this.router.navigate(["/remove-password"]); + }); + } else { + msg.webExtSender = sender; + this.broadcasterService.send(msg); + } + }; + + BrowserApi.messageListener("app.component", (window as any).bitwardenPopupMainMessageListener); + + this.router.events.subscribe(async (event) => { + if (event instanceof NavigationEnd) { + const url = event.urlAfterRedirects || event.url || ""; + if ( + url.startsWith("/tabs/") && + (window as any).previousPopupUrl != null && + (window as any).previousPopupUrl.startsWith("/tabs/") + ) { + await this.stateService.setBrowserGroupingComponentState(null); + await this.stateService.setBrowserCipherComponentState(null); + await this.stateService.setBrowserSendComponentState(null); + await this.stateService.setBrowserSendTypeComponentState(null); + } + if (url.startsWith("/tabs/")) { + await this.stateService.setAddEditCipherInfo(null); + } + (window as any).previousPopupUrl = url; + + // Clear route direction after animation (400ms) + if ((window as any).routeDirection != null) { + window.setTimeout(() => { + (window as any).routeDirection = null; + }, 400); + } + } + }); + } + + getState(outlet: RouterOutlet) { + if (outlet.activatedRouteData.state === "ciphers") { + const routeDirection = + (window as any).routeDirection != null ? (window as any).routeDirection : ""; + return ( + "ciphers_direction=" + + routeDirection + + "_" + + (outlet.activatedRoute.queryParams as any).value.folderId + + "_" + + (outlet.activatedRoute.queryParams as any).value.collectionId + ); + } else { + return outlet.activatedRouteData.state; } + } + + private async recordActivity() { + const now = new Date().getTime(); + if (this.lastActivity != null && now - this.lastActivity < 250) { + return; + } + + this.lastActivity = now; + await this.stateService.setLastActive(now); + } + + private showToast(msg: any) { + let message = ""; + + const options: Partial = {}; + + if (typeof msg.text === "string") { + message = msg.text; + } else if (msg.text.length === 1) { + message = msg.text[0]; + } else { + msg.text.forEach( + (t: string) => + (message += "

" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "

") + ); + options.enableHtml = true; + } + if (msg.options != null) { + if (msg.options.trustedHtml === true) { + options.enableHtml = true; + } + if (msg.options.timeout != null && msg.options.timeout > 0) { + options.timeOut = msg.options.timeout; + } + } + + this.toastrService.show(message, msg.title, options, "toast-" + msg.type); + } + + private async showDialog(msg: any) { + let iconClasses: string = null; + const type = msg.type; + if (type != null) { + // If you add custom types to this part, the type to SweetAlertIcon cast below needs to be changed. + switch (type) { + case "success": + iconClasses = "fa-check text-success"; + break; + case "warning": + iconClasses = "fa-warning text-warning"; + break; + case "error": + iconClasses = "fa-bolt text-danger"; + break; + case "info": + iconClasses = "fa-info-circle text-info"; + break; + default: + break; + } + } + + const cancelText = msg.cancelText; + const confirmText = msg.confirmText; + const confirmed = await Swal.fire({ + heightAuto: false, + buttonsStyling: false, + icon: type as SweetAlertIcon, // required to be any of the SweetAlertIcons to output the iconHtml. + iconHtml: + iconClasses != null ? `` : undefined, + text: msg.text, + html: msg.html, + titleText: msg.title, + showCancelButton: cancelText != null, + cancelButtonText: cancelText, + showConfirmButton: true, + confirmButtonText: confirmText == null ? this.i18nService.t("ok") : confirmText, + timer: 300000, + }); + + this.messagingService.send("showDialogResolve", { + dialogId: msg.dialogId, + confirmed: confirmed.value, + }); + } } diff --git a/src/popup/components/action-buttons.component.ts b/src/popup/components/action-buttons.component.ts index 151f46f98b..2ecdd32e37 100644 --- a/src/popup/components/action-buttons.component.ts +++ b/src/popup/components/action-buttons.component.ts @@ -1,83 +1,89 @@ -import { - Component, - EventEmitter, - Input, - Output, -} from '@angular/core'; +import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { CipherRepromptType } from 'jslib-common/enums/cipherRepromptType'; -import { CipherType } from 'jslib-common/enums/cipherType'; -import { EventType } from 'jslib-common/enums/eventType'; +import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType"; +import { CipherType } from "jslib-common/enums/cipherType"; +import { EventType } from "jslib-common/enums/eventType"; -import { CipherView } from 'jslib-common/models/view/cipherView'; +import { CipherView } from "jslib-common/models/view/cipherView"; -import { EventService } from 'jslib-common/abstractions/event.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { TotpService } from 'jslib-common/abstractions/totp.service'; +import { EventService } from "jslib-common/abstractions/event.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { TotpService } from "jslib-common/abstractions/totp.service"; -import { StateService } from '../../services/abstractions/state.service'; +import { StateService } from "../../services/abstractions/state.service"; @Component({ - selector: 'app-action-buttons', - templateUrl: 'action-buttons.component.html', + selector: "app-action-buttons", + templateUrl: "action-buttons.component.html", }) export class ActionButtonsComponent { - @Output() onView = new EventEmitter(); - @Output() launchEvent = new EventEmitter(); - @Input() cipher: CipherView; - @Input() showView = false; + @Output() onView = new EventEmitter(); + @Output() launchEvent = new EventEmitter(); + @Input() cipher: CipherView; + @Input() showView = false; - cipherType = CipherType; - userHasPremiumAccess = false; + cipherType = CipherType; + userHasPremiumAccess = false; - constructor(private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private eventService: EventService, - private totpService: TotpService, private stateService: StateService, - private passwordRepromptService: PasswordRepromptService) { } + constructor( + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private eventService: EventService, + private totpService: TotpService, + private stateService: StateService, + private passwordRepromptService: PasswordRepromptService + ) {} - async ngOnInit() { - this.userHasPremiumAccess = await this.stateService.getCanAccessPremium(); + async ngOnInit() { + this.userHasPremiumAccess = await this.stateService.getCanAccessPremium(); + } + + launchCipher() { + this.launchEvent.emit(this.cipher); + } + + async copy(cipher: CipherView, value: string, typeI18nKey: string, aType: string) { + if ( + this.cipher.reprompt !== CipherRepromptType.None && + this.passwordRepromptService.protectedFields().includes(aType) && + !(await this.passwordRepromptService.showPasswordPrompt()) + ) { + return; } - launchCipher() { - this.launchEvent.emit(this.cipher); + if (value == null || (aType === "TOTP" && !this.displayTotpCopyButton(cipher))) { + return; + } else if (value === cipher.login.totp) { + value = await this.totpService.getCode(value); } - async copy(cipher: CipherView, value: string, typeI18nKey: string, aType: string) { - if (this.cipher.reprompt !== CipherRepromptType.None && this.passwordRepromptService.protectedFields().includes(aType) && - !await this.passwordRepromptService.showPasswordPrompt()) { - return; - } - - if (value == null || aType === 'TOTP' && !this.displayTotpCopyButton(cipher)) { - return; - } else if (value === cipher.login.totp) { - value = await this.totpService.getCode(value); - } - - if (!cipher.viewPassword) { - return; - } - - this.platformUtilsService.copyToClipboard(value, { window: window }); - this.platformUtilsService.showToast('info', null, - this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey))); - - if (typeI18nKey === 'password' || typeI18nKey === 'verificationCodeTotp') { - this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, cipher.id); - } else if (typeI18nKey === 'securityCode') { - this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id); - } + if (!cipher.viewPassword) { + return; } - displayTotpCopyButton(cipher: CipherView) { - return (cipher?.login?.hasTotp ?? false) && - (cipher.organizationUseTotp || this.userHasPremiumAccess); - } + this.platformUtilsService.copyToClipboard(value, { window: window }); + this.platformUtilsService.showToast( + "info", + null, + this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)) + ); - view() { - this.onView.emit(this.cipher); + if (typeI18nKey === "password" || typeI18nKey === "verificationCodeTotp") { + this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, cipher.id); + } else if (typeI18nKey === "securityCode") { + this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id); } + } + + displayTotpCopyButton(cipher: CipherView) { + return ( + (cipher?.login?.hasTotp ?? false) && (cipher.organizationUseTotp || this.userHasPremiumAccess) + ); + } + + view() { + this.onView.emit(this.cipher); + } } diff --git a/src/popup/generator/password-generator.component.ts b/src/popup/generator/password-generator.component.ts index 47d14c2046..64b3edb98d 100644 --- a/src/popup/generator/password-generator.component.ts +++ b/src/popup/generator/password-generator.component.ts @@ -1,46 +1,48 @@ -import { Location } from '@angular/common'; -import { Component } from '@angular/core'; +import { Location } from "@angular/common"; +import { Component } from "@angular/core"; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StateService } from '../../services/abstractions/state.service'; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "../../services/abstractions/state.service"; -import { CipherView } from 'jslib-common/models/view/cipherView'; +import { CipherView } from "jslib-common/models/view/cipherView"; -import { - PasswordGeneratorComponent as BasePasswordGeneratorComponent, -} from 'jslib-angular/components/password-generator.component'; +import { PasswordGeneratorComponent as BasePasswordGeneratorComponent } from "jslib-angular/components/password-generator.component"; @Component({ - selector: 'app-password-generator', - templateUrl: 'password-generator.component.html', + selector: "app-password-generator", + templateUrl: "password-generator.component.html", }) export class PasswordGeneratorComponent extends BasePasswordGeneratorComponent { - private cipherState: CipherView; + private cipherState: CipherView; - constructor(passwordGenerationService: PasswordGenerationService, platformUtilsService: PlatformUtilsService, - i18nService: I18nService, private stateService: StateService, - private location: Location) { - super(passwordGenerationService, platformUtilsService, i18nService, window); - } + constructor( + passwordGenerationService: PasswordGenerationService, + platformUtilsService: PlatformUtilsService, + i18nService: I18nService, + private stateService: StateService, + private location: Location + ) { + super(passwordGenerationService, platformUtilsService, i18nService, window); + } - async ngOnInit() { - await super.ngOnInit(); - const addEditCipherInfo = await this.stateService.getAddEditCipherInfo(); - if (addEditCipherInfo != null) { - this.cipherState = addEditCipherInfo.cipher; - } - this.showSelect = this.cipherState != null; + async ngOnInit() { + await super.ngOnInit(); + const addEditCipherInfo = await this.stateService.getAddEditCipherInfo(); + if (addEditCipherInfo != null) { + this.cipherState = addEditCipherInfo.cipher; } + this.showSelect = this.cipherState != null; + } - select() { - super.select(); - this.cipherState.login.password = this.password; - this.close(); - } + select() { + super.select(); + this.cipherState.login.password = this.password; + this.close(); + } - close() { - this.location.back(); - } + close() { + this.location.back(); + } } diff --git a/src/popup/send/send-add-edit.component.ts b/src/popup/send/send-add-edit.component.ts index 924300a760..7744f6d1d1 100644 --- a/src/popup/send/send-add-edit.component.ts +++ b/src/popup/send/send-add-edit.component.ts @@ -1,129 +1,150 @@ -import { - DatePipe, - Location, -} from '@angular/common'; +import { DatePipe, Location } from "@angular/common"; -import { Component } from '@angular/core'; +import { Component } from "@angular/core"; -import { - ActivatedRoute, - Router, -} from '@angular/router'; +import { ActivatedRoute, Router } from "@angular/router"; -import { first } from 'rxjs/operators'; +import { first } from "rxjs/operators"; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService } from 'jslib-common/abstractions/policy.service'; -import { SendService } from 'jslib-common/abstractions/send.service'; -import { TokenService } from 'jslib-common/abstractions/token.service'; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService } from "jslib-common/abstractions/policy.service"; +import { SendService } from "jslib-common/abstractions/send.service"; +import { TokenService } from "jslib-common/abstractions/token.service"; -import { PopupUtilsService } from '../services/popup-utils.service'; +import { PopupUtilsService } from "../services/popup-utils.service"; -import { AddEditComponent as BaseAddEditComponent } from 'jslib-angular/components/send/add-edit.component'; -import { StateService } from '../../services/abstractions/state.service'; +import { AddEditComponent as BaseAddEditComponent } from "jslib-angular/components/send/add-edit.component"; +import { StateService } from "../../services/abstractions/state.service"; @Component({ - selector: 'app-send-add-edit', - templateUrl: 'send-add-edit.component.html', + selector: "app-send-add-edit", + templateUrl: "send-add-edit.component.html", }) export class SendAddEditComponent extends BaseAddEditComponent { - // Options header - showOptions = false; - // File visibility - isFirefox = false; - inPopout = false; - inSidebar = false; - isLinux = false; - isUnsupportedMac = false; + // Options header + showOptions = false; + // File visibility + isFirefox = false; + inPopout = false; + inSidebar = false; + isLinux = false; + isUnsupportedMac = false; - constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService, - stateService: StateService, messagingService: MessagingService, policyService: PolicyService, - environmentService: EnvironmentService, datePipe: DatePipe, sendService: SendService, - private route: ActivatedRoute, private router: Router, private location: Location, - private popupUtilsService: PopupUtilsService, logService: LogService) { - super(i18nService, platformUtilsService, environmentService, datePipe, - sendService, messagingService, policyService, logService, stateService); + constructor( + i18nService: I18nService, + platformUtilsService: PlatformUtilsService, + stateService: StateService, + messagingService: MessagingService, + policyService: PolicyService, + environmentService: EnvironmentService, + datePipe: DatePipe, + sendService: SendService, + private route: ActivatedRoute, + private router: Router, + private location: Location, + private popupUtilsService: PopupUtilsService, + logService: LogService + ) { + super( + i18nService, + platformUtilsService, + environmentService, + datePipe, + sendService, + messagingService, + policyService, + logService, + stateService + ); + } + + get showFileSelector(): boolean { + return !(this.editMode || this.showFilePopoutMessage); + } + + get showFilePopoutMessage(): boolean { + return ( + !this.editMode && + (this.showFirefoxFileWarning || this.showSafariFileWarning || this.showChromiumFileWarning) + ); + } + + get showFirefoxFileWarning(): boolean { + return this.isFirefox && !(this.inSidebar || this.inPopout); + } + + get showSafariFileWarning(): boolean { + return this.isSafari && !this.inPopout; + } + + // Only show this for Chromium based browsers in Linux and Mac > Big Sur + get showChromiumFileWarning(): boolean { + return ( + (this.isLinux || this.isUnsupportedMac) && + !this.isFirefox && + !(this.inSidebar || this.inPopout) + ); + } + + popOutWindow() { + this.popupUtilsService.popOut(window); + } + + async ngOnInit() { + // File visilibity + this.isFirefox = this.platformUtilsService.isFirefox(); + this.inPopout = this.popupUtilsService.inPopout(window); + this.inSidebar = this.popupUtilsService.inSidebar(window); + this.isLinux = window?.navigator?.userAgent.indexOf("Linux") !== -1; + this.isUnsupportedMac = + this.platformUtilsService.isChrome() && window?.navigator?.appVersion.includes("Mac OS X 11"); + + this.route.queryParams.pipe(first()).subscribe(async (params) => { + if (params.sendId) { + this.sendId = params.sendId; + } + if (params.type) { + const type = parseInt(params.type, null); + this.type = type; + } + await this.load(); + }); + + window.setTimeout(() => { + if (!this.editMode) { + document.getElementById("name").focus(); + } + }, 200); + } + + async submit(): Promise { + if (await super.submit()) { + this.cancel(); + return true; } - get showFileSelector(): boolean { - return !(this.editMode || this.showFilePopoutMessage); + return false; + } + + async delete(): Promise { + if (await super.delete()) { + this.cancel(); + return true; } - get showFilePopoutMessage(): boolean { - return !this.editMode && (this.showFirefoxFileWarning || this.showSafariFileWarning || this.showChromiumFileWarning); - } - - get showFirefoxFileWarning(): boolean { - return this.isFirefox && !(this.inSidebar || this.inPopout); - } - - get showSafariFileWarning(): boolean { - return this.isSafari && !this.inPopout; - } - - // Only show this for Chromium based browsers in Linux and Mac > Big Sur - get showChromiumFileWarning(): boolean { - return (this.isLinux || this.isUnsupportedMac) && !this.isFirefox && !(this.inSidebar || this.inPopout); - } - - popOutWindow() { - this.popupUtilsService.popOut(window); - } - - async ngOnInit() { - // File visilibity - this.isFirefox = this.platformUtilsService.isFirefox(); - this.inPopout = this.popupUtilsService.inPopout(window); - this.inSidebar = this.popupUtilsService.inSidebar(window); - this.isLinux = window?.navigator?.userAgent.indexOf('Linux') !== -1; - this.isUnsupportedMac = this.platformUtilsService.isChrome() && window?.navigator?.appVersion.includes('Mac OS X 11'); - - this.route.queryParams.pipe(first()).subscribe(async params => { - if (params.sendId) { - this.sendId = params.sendId; - } - if (params.type) { - const type = parseInt(params.type, null); - this.type = type; - } - await this.load(); - }); - - window.setTimeout(() => { - if (!this.editMode) { - document.getElementById('name').focus(); - } - }, 200); - } - - async submit(): Promise { - if (await super.submit()) { - this.cancel(); - return true; - } - - return false; - } - - async delete(): Promise { - if (await super.delete()) { - this.cancel(); - return true; - } - - return false; - } - - cancel() { - // If true, the window was pop'd out on the add-send page. location.back will not work - if ((window as any).previousPopupUrl.startsWith('/add-send')) { - this.router.navigate(['tabs/send']); - } else { - this.location.back(); - } + return false; + } + + cancel() { + // If true, the window was pop'd out on the add-send page. location.back will not work + if ((window as any).previousPopupUrl.startsWith("/add-send")) { + this.router.navigate(["tabs/send"]); + } else { + this.location.back(); } + } } diff --git a/src/popup/send/send-groupings.component.ts b/src/popup/send/send-groupings.component.ts index 6c18f0e011..6b358fb9de 100644 --- a/src/popup/send/send-groupings.component.ts +++ b/src/popup/send/send-groupings.component.ts @@ -1,185 +1,200 @@ -import { - ChangeDetectorRef, - Component, - NgZone, -} from '@angular/core'; +import { ChangeDetectorRef, Component, NgZone } from "@angular/core"; -import { - Router, -} from '@angular/router'; +import { Router } from "@angular/router"; -import { SendView } from 'jslib-common/models/view/sendView'; +import { SendView } from "jslib-common/models/view/sendView"; -import { SendComponent as BaseSendComponent } from 'jslib-angular/components/send/send.component'; +import { SendComponent as BaseSendComponent } from "jslib-angular/components/send/send.component"; -import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService } from 'jslib-common/abstractions/policy.service'; -import { SearchService } from 'jslib-common/abstractions/search.service'; -import { SendService } from 'jslib-common/abstractions/send.service'; -import { SyncService } from 'jslib-common/abstractions/sync.service'; +import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService } from "jslib-common/abstractions/policy.service"; +import { SearchService } from "jslib-common/abstractions/search.service"; +import { SendService } from "jslib-common/abstractions/send.service"; +import { SyncService } from "jslib-common/abstractions/sync.service"; -import { StateService } from '../../services/abstractions/state.service'; +import { StateService } from "../../services/abstractions/state.service"; -import { PopupUtilsService } from '../services/popup-utils.service'; +import { PopupUtilsService } from "../services/popup-utils.service"; -import { SendType } from 'jslib-common/enums/sendType'; -import { BrowserSendComponentState } from '../../models/browserSendComponentState'; +import { SendType } from "jslib-common/enums/sendType"; +import { BrowserSendComponentState } from "../../models/browserSendComponentState"; -const ComponentId = 'SendComponent'; -const ScopeStateId = ComponentId + 'Scope'; +const ComponentId = "SendComponent"; +const ScopeStateId = ComponentId + "Scope"; @Component({ - selector: 'app-send-groupings', - templateUrl: 'send-groupings.component.html', + selector: "app-send-groupings", + templateUrl: "send-groupings.component.html", }) export class SendGroupingsComponent extends BaseSendComponent { - // Header - showLeftHeader = true; - // Send Type Calculations - typeCounts = new Map(); - // State Handling - state: BrowserSendComponentState; - private loadedTimeout: number; + // Header + showLeftHeader = true; + // Send Type Calculations + typeCounts = new Map(); + // State Handling + state: BrowserSendComponentState; + private loadedTimeout: number; - constructor(sendService: SendService, i18nService: I18nService, - platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, ngZone: NgZone, - policyService: PolicyService, searchService: SearchService, - private popupUtils: PopupUtilsService, private stateService: StateService, - private router: Router, private syncService: SyncService, - private changeDetectorRef: ChangeDetectorRef, private broadcasterService: BroadcasterService, - logService: LogService) { - super(sendService, i18nService, platformUtilsService, environmentService, ngZone, searchService, - policyService, logService); - super.onSuccessfulLoad = async () => { - this.calculateTypeCounts(); - this.selectAll(); - }; + constructor( + sendService: SendService, + i18nService: I18nService, + platformUtilsService: PlatformUtilsService, + environmentService: EnvironmentService, + ngZone: NgZone, + policyService: PolicyService, + searchService: SearchService, + private popupUtils: PopupUtilsService, + private stateService: StateService, + private router: Router, + private syncService: SyncService, + private changeDetectorRef: ChangeDetectorRef, + private broadcasterService: BroadcasterService, + logService: LogService + ) { + super( + sendService, + i18nService, + platformUtilsService, + environmentService, + ngZone, + searchService, + policyService, + logService + ); + super.onSuccessfulLoad = async () => { + this.calculateTypeCounts(); + this.selectAll(); + }; + } + + async ngOnInit() { + // Determine Header details + this.showLeftHeader = !( + this.popupUtils.inSidebar(window) && this.platformUtilsService.isFirefox() + ); + // Clear state of Send Type Component + this.stateService.setBrowserSendComponentState(null); + // Let super class finish + await super.ngOnInit(); + // Handle State Restore if necessary + const restoredScopeState = await this.restoreState(); + if (this.state?.searchText != null) { + this.searchText = this.state.searchText; } - async ngOnInit() { - // Determine Header details - this.showLeftHeader = !(this.popupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()); - // Clear state of Send Type Component - this.stateService.setBrowserSendComponentState(null); - // Let super class finish - await super.ngOnInit(); - // Handle State Restore if necessary - const restoredScopeState = await this.restoreState(); - if (this.state?.searchText != null) { - this.searchText = this.state.searchText; + if (!this.syncService.syncInProgress) { + this.load(); + } else { + this.loadedTimeout = window.setTimeout(() => { + if (!this.loaded) { + this.load(); + } + }, 5000); + } + + if (!this.syncService.syncInProgress || restoredScopeState) { + window.setTimeout(() => this.popupUtils.setContentScrollY(window, this.state.scrollY), 0); + } + + // Load all sends if sync completed in background + this.broadcasterService.subscribe(ComponentId, (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case "syncCompleted": + window.setTimeout(() => { + this.load(); + }, 500); + break; + default: + break; } - if (!this.syncService.syncInProgress) { - this.load(); - } else { - this.loadedTimeout = window.setTimeout(() => { - if (!this.loaded) { - this.load(); - } - }, 5000); - } + this.changeDetectorRef.detectChanges(); + }); + }); + } - if (!this.syncService.syncInProgress || restoredScopeState) { - window.setTimeout(() => this.popupUtils.setContentScrollY(window, this.state.scrollY), 0); - } + ngOnDestroy() { + // Remove timeout + if (this.loadedTimeout != null) { + window.clearTimeout(this.loadedTimeout); + } + // Save state + this.saveState(); + // Unsubscribe + this.broadcasterService.unsubscribe(ComponentId); + } - // Load all sends if sync completed in background - this.broadcasterService.subscribe(ComponentId, (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case 'syncCompleted': - window.setTimeout(() => { - this.load(); - }, 500); - break; - default: - break; - } + async selectType(type: SendType) { + this.router.navigate(["/send-type"], { queryParams: { type: type } }); + } - this.changeDetectorRef.detectChanges(); - }); - }); + async selectSend(s: SendView) { + this.router.navigate(["/edit-send"], { queryParams: { sendId: s.id } }); + } + + async addSend() { + if (this.disableSend) { + return; + } + this.router.navigate(["/add-send"]); + } + + async removePassword(s: SendView): Promise { + if (this.disableSend) { + return; + } + super.removePassword(s); + } + + showSearching() { + return ( + this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText)) + ); + } + + private calculateTypeCounts() { + // Create type counts + const typeCounts = new Map(); + this.sends.forEach((s) => { + if (typeCounts.has(s.type)) { + typeCounts.set(s.type, typeCounts.get(s.type) + 1); + } else { + typeCounts.set(s.type, 1); + } + }); + this.typeCounts = typeCounts; + } + + private async saveState() { + this.state = { + scrollY: this.popupUtils.getContentScrollY(window), + searchText: this.searchText, + sends: this.sends, + typeCounts: this.typeCounts, + }; + await this.stateService.setBrowserSendComponentState(this.state); + } + + private async restoreState(): Promise { + this.state = await this.stateService.getBrowserSendComponentState(); + if (this.state == null) { + return false; + } + if (this.state.sends != null) { + this.sends = this.state.sends; + } + if (this.state.typeCounts != null) { + this.typeCounts = this.state.typeCounts; + } + if (this.state.searchText != null) { + this.searchText = this.state.searchText; } - ngOnDestroy() { - // Remove timeout - if (this.loadedTimeout != null) { - window.clearTimeout(this.loadedTimeout); - } - // Save state - this.saveState(); - // Unsubscribe - this.broadcasterService.unsubscribe(ComponentId); - } - - async selectType(type: SendType) { - this.router.navigate(['/send-type'], { queryParams: { type: type } }); - } - - async selectSend(s: SendView) { - this.router.navigate(['/edit-send'], { queryParams: { sendId: s.id } }); - } - - async addSend() { - if (this.disableSend) { - return; - } - this.router.navigate(['/add-send']); - } - - async removePassword(s: SendView): Promise { - if (this.disableSend) { - return; - } - super.removePassword(s); - } - - showSearching() { - return this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText)); - } - - private calculateTypeCounts() { - // Create type counts - const typeCounts = new Map(); - this.sends.forEach(s => { - if (typeCounts.has(s.type)) { - typeCounts.set(s.type, typeCounts.get(s.type) + 1); - } else { - typeCounts.set(s.type, 1); - } - }); - this.typeCounts = typeCounts; - } - - private async saveState() { - this.state = { - scrollY: this.popupUtils.getContentScrollY(window), - searchText: this.searchText, - sends: this.sends, - typeCounts: this.typeCounts, - }; - await this.stateService.setBrowserSendComponentState(this.state); - } - - private async restoreState(): Promise { - this.state = await this.stateService.getBrowserSendComponentState(); - if (this.state == null) { - return false; - } - if (this.state.sends != null) { - this.sends = this.state.sends; - } - if (this.state.typeCounts != null) { - this.typeCounts = this.state.typeCounts; - } - if (this.state.searchText != null) { - this.searchText = this.state.searchText; - } - - return true; - } + return true; + } } diff --git a/src/popup/send/send-type.component.ts b/src/popup/send/send-type.component.ts index 64c8ab57de..eadad2054b 100644 --- a/src/popup/send/send-type.component.ts +++ b/src/popup/send/send-type.component.ts @@ -1,158 +1,170 @@ -import { - ChangeDetectorRef, - Component, - NgZone, -} from '@angular/core'; +import { ChangeDetectorRef, Component, NgZone } from "@angular/core"; -import { - ActivatedRoute, - Router, -} from '@angular/router'; +import { ActivatedRoute, Router } from "@angular/router"; -import { first } from 'rxjs/operators'; +import { first } from "rxjs/operators"; -import { Location } from '@angular/common'; +import { Location } from "@angular/common"; -import { SendView } from 'jslib-common/models/view/sendView'; +import { SendView } from "jslib-common/models/view/sendView"; -import { SendComponent as BaseSendComponent } from 'jslib-angular/components/send/send.component'; +import { SendComponent as BaseSendComponent } from "jslib-angular/components/send/send.component"; -import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService } from 'jslib-common/abstractions/policy.service'; -import { SearchService } from 'jslib-common/abstractions/search.service'; -import { SendService } from 'jslib-common/abstractions/send.service'; -import { StateService } from '../../services/abstractions/state.service'; +import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService } from "jslib-common/abstractions/policy.service"; +import { SearchService } from "jslib-common/abstractions/search.service"; +import { SendService } from "jslib-common/abstractions/send.service"; +import { StateService } from "../../services/abstractions/state.service"; -import { PopupUtilsService } from '../services/popup-utils.service'; +import { PopupUtilsService } from "../services/popup-utils.service"; -import { SendType } from 'jslib-common/enums/sendType'; -import { BrowserComponentState } from '../../models/browserComponentState'; +import { SendType } from "jslib-common/enums/sendType"; +import { BrowserComponentState } from "../../models/browserComponentState"; -const ComponentId = 'SendTypeComponent'; +const ComponentId = "SendTypeComponent"; @Component({ - selector: 'app-send-type', - templateUrl: 'send-type.component.html', + selector: "app-send-type", + templateUrl: "send-type.component.html", }) export class SendTypeComponent extends BaseSendComponent { - groupingTitle: string; - // State Handling - state: BrowserComponentState; - private refreshTimeout: number; - private applySavedState = true; + groupingTitle: string; + // State Handling + state: BrowserComponentState; + private refreshTimeout: number; + private applySavedState = true; - constructor(sendService: SendService, i18nService: I18nService, - platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, ngZone: NgZone, - policyService: PolicyService, searchService: SearchService, - private popupUtils: PopupUtilsService, private stateService: StateService, - private route: ActivatedRoute, private location: Location, private changeDetectorRef: ChangeDetectorRef, - private broadcasterService: BroadcasterService, private router: Router, logService: LogService) { - super(sendService, i18nService, platformUtilsService, environmentService, ngZone, searchService, - policyService, logService); - super.onSuccessfulLoad = async () => { - this.selectType(this.type); - }; - this.applySavedState = (window as any).previousPopupUrl != null && - !(window as any).previousPopupUrl.startsWith('/send-type'); - } + constructor( + sendService: SendService, + i18nService: I18nService, + platformUtilsService: PlatformUtilsService, + environmentService: EnvironmentService, + ngZone: NgZone, + policyService: PolicyService, + searchService: SearchService, + private popupUtils: PopupUtilsService, + private stateService: StateService, + private route: ActivatedRoute, + private location: Location, + private changeDetectorRef: ChangeDetectorRef, + private broadcasterService: BroadcasterService, + private router: Router, + logService: LogService + ) { + super( + sendService, + i18nService, + platformUtilsService, + environmentService, + ngZone, + searchService, + policyService, + logService + ); + super.onSuccessfulLoad = async () => { + this.selectType(this.type); + }; + this.applySavedState = + (window as any).previousPopupUrl != null && + !(window as any).previousPopupUrl.startsWith("/send-type"); + } - async ngOnInit() { - // Let super class finish - await super.ngOnInit(); - this.route.queryParams.pipe(first()).subscribe(async params => { - if (this.applySavedState) { - this.state = (await this.stateService.getBrowserSendTypeComponentState()); - if (this.state.searchText != null) { - this.searchText = this.state.searchText; - } - } - - if (params.type != null) { - this.type = parseInt(params.type, null); - switch (this.type) { - case SendType.Text: - this.groupingTitle = this.i18nService.t('sendTypeText'); - break; - case SendType.File: - this.groupingTitle = this.i18nService.t('sendTypeFile'); - break; - default: - break; - } - await this.load(s => s.type === this.type); - } - - // Restore state and remove reference - if (this.applySavedState && this.state != null) { - window.setTimeout(() => this.popupUtils.setContentScrollY(window, this.state?.scrollY), 0); - } - this.stateService.setBrowserSendComponentState(null); - - }); - - // Refresh Send list if sync completed in background - this.broadcasterService.subscribe(ComponentId, (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case 'syncCompleted': - if (message.successfully) { - this.refreshTimeout = window.setTimeout(() => { - this.refresh(); - }, 500); - } - break; - default: - break; - } - - this.changeDetectorRef.detectChanges(); - }); - }); - } - - ngOnDestroy() { - // Remove timeout - if (this.refreshTimeout != null) { - window.clearTimeout(this.refreshTimeout); + async ngOnInit() { + // Let super class finish + await super.ngOnInit(); + this.route.queryParams.pipe(first()).subscribe(async (params) => { + if (this.applySavedState) { + this.state = await this.stateService.getBrowserSendTypeComponentState(); + if (this.state.searchText != null) { + this.searchText = this.state.searchText; } - // Save state - this.saveState(); - // Unsubscribe - this.broadcasterService.unsubscribe(ComponentId); - } + } - async selectSend(s: SendView) { - this.router.navigate(['/edit-send'], { queryParams: { sendId: s.id } }); - } - - async addSend() { - if (this.disableSend) { - return; + if (params.type != null) { + this.type = parseInt(params.type, null); + switch (this.type) { + case SendType.Text: + this.groupingTitle = this.i18nService.t("sendTypeText"); + break; + case SendType.File: + this.groupingTitle = this.i18nService.t("sendTypeFile"); + break; + default: + break; } - this.router.navigate(['/add-send'], { queryParams: { type: this.type } }); - } + await this.load((s) => s.type === this.type); + } - async removePassword(s: SendView): Promise { - if (this.disableSend) { - return; + // Restore state and remove reference + if (this.applySavedState && this.state != null) { + window.setTimeout(() => this.popupUtils.setContentScrollY(window, this.state?.scrollY), 0); + } + this.stateService.setBrowserSendComponentState(null); + }); + + // Refresh Send list if sync completed in background + this.broadcasterService.subscribe(ComponentId, (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case "syncCompleted": + if (message.successfully) { + this.refreshTimeout = window.setTimeout(() => { + this.refresh(); + }, 500); + } + break; + default: + break; } - super.removePassword(s); - } - back() { - (window as any).routeDirection = 'b'; - this.location.back(); - } + this.changeDetectorRef.detectChanges(); + }); + }); + } - private async saveState() { - this.state = { - scrollY: this.popupUtils.getContentScrollY(window), - searchText: this.searchText, - }; - await this.stateService.setBrowserSendTypeComponentState(this.state); + ngOnDestroy() { + // Remove timeout + if (this.refreshTimeout != null) { + window.clearTimeout(this.refreshTimeout); } + // Save state + this.saveState(); + // Unsubscribe + this.broadcasterService.unsubscribe(ComponentId); + } + + async selectSend(s: SendView) { + this.router.navigate(["/edit-send"], { queryParams: { sendId: s.id } }); + } + + async addSend() { + if (this.disableSend) { + return; + } + this.router.navigate(["/add-send"], { queryParams: { type: this.type } }); + } + + async removePassword(s: SendView): Promise { + if (this.disableSend) { + return; + } + super.removePassword(s); + } + + back() { + (window as any).routeDirection = "b"; + this.location.back(); + } + + private async saveState() { + this.state = { + scrollY: this.popupUtils.getContentScrollY(window), + searchText: this.searchText, + }; + await this.stateService.setBrowserSendTypeComponentState(this.state); + } } diff --git a/src/popup/services/services.module.ts b/src/popup/services/services.module.ts index 9b788c5b87..d9b5e39518 100644 --- a/src/popup/services/services.module.ts +++ b/src/popup/services/services.module.ts @@ -1,223 +1,282 @@ -import { - APP_INITIALIZER, - LOCALE_ID, - NgModule, -} from '@angular/core'; +import { APP_INITIALIZER, LOCALE_ID, NgModule } from "@angular/core"; -import { DebounceNavigationService } from './debounceNavigationService'; -import { LaunchGuardService } from './launch-guard.service'; -import { LockGuardService } from './lock-guard.service'; -import { PasswordRepromptService } from './password-reprompt.service'; -import { UnauthGuardService } from './unauth-guard.service'; +import { DebounceNavigationService } from "./debounceNavigationService"; +import { LaunchGuardService } from "./launch-guard.service"; +import { LockGuardService } from "./lock-guard.service"; +import { PasswordRepromptService } from "./password-reprompt.service"; +import { UnauthGuardService } from "./unauth-guard.service"; -import { JslibServicesModule } from 'jslib-angular/services/jslib-services.module'; -import { LockGuardService as BaseLockGuardService } from 'jslib-angular/services/lock-guard.service'; -import { UnauthGuardService as BaseUnauthGuardService } from 'jslib-angular/services/unauth-guard.service'; +import { JslibServicesModule } from "jslib-angular/services/jslib-services.module"; +import { LockGuardService as BaseLockGuardService } from "jslib-angular/services/lock-guard.service"; +import { UnauthGuardService as BaseUnauthGuardService } from "jslib-angular/services/unauth-guard.service"; -import { BrowserApi } from '../../browser/browserApi'; +import { BrowserApi } from "../../browser/browserApi"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { AppIdService } from 'jslib-common/abstractions/appId.service'; -import { AuditService } from 'jslib-common/abstractions/audit.service'; -import { AuthService as AuthServiceAbstraction } from 'jslib-common/abstractions/auth.service'; -import { CipherService } from 'jslib-common/abstractions/cipher.service'; -import { CollectionService } from 'jslib-common/abstractions/collection.service'; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { EventService } from 'jslib-common/abstractions/event.service'; -import { ExportService } from 'jslib-common/abstractions/export.service'; -import { FileUploadService } from 'jslib-common/abstractions/fileUpload.service'; -import { FolderService } from 'jslib-common/abstractions/folder.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; -import { LogService as LogServiceAbstraction } from 'jslib-common/abstractions/log.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { NotificationsService } from 'jslib-common/abstractions/notifications.service'; -import { OrganizationService } from 'jslib-common/abstractions/organization.service'; -import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; -import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from 'jslib-common/abstractions/passwordReprompt.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService } from 'jslib-common/abstractions/policy.service'; -import { ProviderService } from 'jslib-common/abstractions/provider.service'; -import { SearchService as SearchServiceAbstraction } from 'jslib-common/abstractions/search.service'; -import { SendService } from 'jslib-common/abstractions/send.service'; -import { SettingsService } from 'jslib-common/abstractions/settings.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { SyncService } from 'jslib-common/abstractions/sync.service'; -import { TokenService } from 'jslib-common/abstractions/token.service'; -import { TotpService } from 'jslib-common/abstractions/totp.service'; -import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service'; -import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { AppIdService } from "jslib-common/abstractions/appId.service"; +import { AuditService } from "jslib-common/abstractions/audit.service"; +import { AuthService as AuthServiceAbstraction } from "jslib-common/abstractions/auth.service"; +import { CipherService } from "jslib-common/abstractions/cipher.service"; +import { CollectionService } from "jslib-common/abstractions/collection.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { EventService } from "jslib-common/abstractions/event.service"; +import { ExportService } from "jslib-common/abstractions/export.service"; +import { FileUploadService } from "jslib-common/abstractions/fileUpload.service"; +import { FolderService } from "jslib-common/abstractions/folder.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; +import { LogService as LogServiceAbstraction } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { NotificationsService } from "jslib-common/abstractions/notifications.service"; +import { OrganizationService } from "jslib-common/abstractions/organization.service"; +import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; +import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "jslib-common/abstractions/passwordReprompt.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService } from "jslib-common/abstractions/policy.service"; +import { ProviderService } from "jslib-common/abstractions/provider.service"; +import { SearchService as SearchServiceAbstraction } from "jslib-common/abstractions/search.service"; +import { SendService } from "jslib-common/abstractions/send.service"; +import { SettingsService } from "jslib-common/abstractions/settings.service"; +import { StorageService } from "jslib-common/abstractions/storage.service"; +import { SyncService } from "jslib-common/abstractions/sync.service"; +import { TokenService } from "jslib-common/abstractions/token.service"; +import { TotpService } from "jslib-common/abstractions/totp.service"; +import { UserVerificationService } from "jslib-common/abstractions/userVerification.service"; +import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; -import { AutofillService } from '../../services/abstractions/autofill.service'; -import BrowserMessagingService from '../../services/browserMessaging.service'; +import { AutofillService } from "../../services/abstractions/autofill.service"; +import BrowserMessagingService from "../../services/browserMessaging.service"; -import { AuthService } from 'jslib-common/services/auth.service'; -import { ConsoleLogService } from 'jslib-common/services/consoleLog.service'; -import { SearchService } from 'jslib-common/services/search.service'; +import { AuthService } from "jslib-common/services/auth.service"; +import { ConsoleLogService } from "jslib-common/services/consoleLog.service"; +import { SearchService } from "jslib-common/services/search.service"; -import { PopupSearchService } from './popup-search.service'; -import { PopupUtilsService } from './popup-utils.service'; +import { PopupSearchService } from "./popup-search.service"; +import { PopupUtilsService } from "./popup-utils.service"; -import { ThemeType } from 'jslib-common/enums/themeType'; -import { StateService } from '../../services/state.service'; +import { ThemeType } from "jslib-common/enums/themeType"; +import { StateService } from "../../services/state.service"; -import { StateService as StateServiceAbstraction } from '../../services/abstractions/state.service'; +import { StateService as StateServiceAbstraction } from "../../services/abstractions/state.service"; function getBgService(service: string) { - return (): T => { - const page = BrowserApi.getBackgroundPage(); - return page ? page.bitwardenMain[service] as T : null; - }; + return (): T => { + const page = BrowserApi.getBackgroundPage(); + return page ? (page.bitwardenMain[service] as T) : null; + }; } const isPrivateMode = BrowserApi.getBackgroundPage() == null; -export function initFactory(platformUtilsService: PlatformUtilsService, i18nService: I18nService, - storageService: StorageService, popupUtilsService: PopupUtilsService, stateService: StateServiceAbstraction, - logService: LogServiceAbstraction): Function { - return async () => { - if (!popupUtilsService.inPopup(window)) { - window.document.body.classList.add('body-full'); - } else if (window.screen.availHeight < 600) { - window.document.body.classList.add('body-xs'); - } else if (window.screen.availHeight <= 800) { - window.document.body.classList.add('body-sm'); - } +export function initFactory( + platformUtilsService: PlatformUtilsService, + i18nService: I18nService, + storageService: StorageService, + popupUtilsService: PopupUtilsService, + stateService: StateServiceAbstraction, + logService: LogServiceAbstraction +): Function { + return async () => { + if (!popupUtilsService.inPopup(window)) { + window.document.body.classList.add("body-full"); + } else if (window.screen.availHeight < 600) { + window.document.body.classList.add("body-xs"); + } else if (window.screen.availHeight <= 800) { + window.document.body.classList.add("body-sm"); + } - if (!isPrivateMode) { - const htmlEl = window.document.documentElement; - const theme = await platformUtilsService.getEffectiveTheme(); - htmlEl.classList.add('theme_' + theme); - platformUtilsService.onDefaultSystemThemeChange(async sysTheme => { - const bwTheme = await stateService.getTheme(); - if (bwTheme == null || bwTheme === ThemeType.System) { - htmlEl.classList.remove('theme_' + ThemeType.Light, 'theme_' + ThemeType.Dark); - htmlEl.classList.add('theme_' + sysTheme); - } - }); - htmlEl.classList.add('locale_' + i18nService.translationLocale); - - // Workaround for slow performance on external monitors on Chrome + MacOS - // See: https://bugs.chromium.org/p/chromium/issues/detail?id=971701#c64 - if (platformUtilsService.isChrome() && - navigator.platform.indexOf('Mac') > -1 && - popupUtilsService.inPopup(window) && - (window.screenLeft < 0 || - window.screenTop < 0 || - window.screenLeft > window.screen.width || - window.screenTop > window.screen.height)) { - htmlEl.classList.add('force_redraw'); - logService.info('Force redraw is on'); - } + if (!isPrivateMode) { + const htmlEl = window.document.documentElement; + const theme = await platformUtilsService.getEffectiveTheme(); + htmlEl.classList.add("theme_" + theme); + platformUtilsService.onDefaultSystemThemeChange(async (sysTheme) => { + const bwTheme = await stateService.getTheme(); + if (bwTheme == null || bwTheme === ThemeType.System) { + htmlEl.classList.remove("theme_" + ThemeType.Light, "theme_" + ThemeType.Dark); + htmlEl.classList.add("theme_" + sysTheme); } - }; + }); + htmlEl.classList.add("locale_" + i18nService.translationLocale); + + // Workaround for slow performance on external monitors on Chrome + MacOS + // See: https://bugs.chromium.org/p/chromium/issues/detail?id=971701#c64 + if ( + platformUtilsService.isChrome() && + navigator.platform.indexOf("Mac") > -1 && + popupUtilsService.inPopup(window) && + (window.screenLeft < 0 || + window.screenTop < 0 || + window.screenLeft > window.screen.width || + window.screenTop > window.screen.height) + ) { + htmlEl.classList.add("force_redraw"); + logService.info("Force redraw is on"); + } + } + }; } @NgModule({ - imports: [ - JslibServicesModule, - ], - declarations: [], - providers: [ - { - provide: LOCALE_ID, - useFactory: () => isPrivateMode ? null : getBgService('i18nService')().translationLocale, - deps: [], - }, - { - provide: APP_INITIALIZER, - useFactory: initFactory, - deps: [ - PlatformUtilsService, - I18nService, - StorageService, - PopupUtilsService, - StateServiceAbstraction, - LogServiceAbstraction, - ], - multi: true, - }, - LaunchGuardService, - { provide: BaseLockGuardService, useClass: LockGuardService }, - { provide: BaseUnauthGuardService, useClass: UnauthGuardService }, - DebounceNavigationService, + imports: [JslibServicesModule], + declarations: [], + providers: [ + { + provide: LOCALE_ID, + useFactory: () => + isPrivateMode ? null : getBgService("i18nService")().translationLocale, + deps: [], + }, + { + provide: APP_INITIALIZER, + useFactory: initFactory, + deps: [ + PlatformUtilsService, + I18nService, + StorageService, PopupUtilsService, - { provide: MessagingService, useClass: BrowserMessagingService }, - { provide: AuthServiceAbstraction, useFactory: getBgService('authService'), deps: [] }, - { provide: StateServiceAbstraction, useFactory: getBgService('stateService') }, - { - provide: SearchServiceAbstraction, - useFactory: (cipherService: CipherService, logService: ConsoleLogService, i18nService: I18nService) => { - return isPrivateMode ? null : new PopupSearchService(getBgService('searchService')(), - cipherService, logService, i18nService); - }, - deps: [ - CipherService, - LogServiceAbstraction, - I18nService, - ], - }, - { provide: AuditService, useFactory: getBgService('auditService'), deps: [] }, - { provide: FileUploadService, useFactory: getBgService('fileUploadService'), deps: [] }, - { provide: CipherService, useFactory: getBgService('cipherService'), deps: [] }, - { - provide: CryptoFunctionService, - useFactory: getBgService('cryptoFunctionService'), - deps: [], - }, - { provide: FolderService, useFactory: getBgService('folderService'), deps: [] }, - { provide: CollectionService, useFactory: getBgService('collectionService'), deps: [] }, - { provide: LogServiceAbstraction, useFactory: getBgService('logService'), deps: [] }, - { provide: EnvironmentService, useFactory: getBgService('environmentService'), deps: [] }, - { provide: TotpService, useFactory: getBgService('totpService'), deps: [] }, - { provide: TokenService, useFactory: getBgService('tokenService'), deps: [] }, - { provide: I18nService, useFactory: getBgService('i18nService'), deps: [] }, - { provide: CryptoService, useFactory: getBgService('cryptoService'), deps: [] }, - { provide: EventService, useFactory: getBgService('eventService'), deps: [] }, - { provide: PolicyService, useFactory: getBgService('policyService'), deps: [] }, - { - provide: PlatformUtilsService, - useFactory: getBgService('platformUtilsService'), - deps: [], - }, - { - provide: PasswordGenerationService, - useFactory: getBgService('passwordGenerationService'), - deps: [], - }, - { provide: ApiService, useFactory: getBgService('apiService'), deps: [] }, - { provide: SyncService, useFactory: getBgService('syncService'), deps: [] }, - { provide: SettingsService, useFactory: getBgService('settingsService'), deps: [] }, - { provide: StorageService, useFactory: getBgService('storageService'), deps: [] }, - { provide: AppIdService, useFactory: getBgService('appIdService'), deps: [] }, - { provide: AutofillService, useFactory: getBgService('autofillService'), deps: [] }, - { provide: ExportService, useFactory: getBgService('exportService'), deps: [] }, - { provide: SendService, useFactory: getBgService('sendService'), deps: [] }, - { provide: KeyConnectorService, useFactory: getBgService('keyConnectorService'), deps: [] }, - { - provide: UserVerificationService, - useFactory: getBgService('userVerificationService'), - deps: [], - }, - { - provide: VaultTimeoutService, - useFactory: getBgService('vaultTimeoutService'), - deps: [], - }, - { - provide: NotificationsService, - useFactory: getBgService('notificationsService'), - deps: [], - }, - { provide: LogServiceAbstraction, useFactory: getBgService('logService'), deps: [] }, - { provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService }, - { provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService }, - { provide: OrganizationService, useFactory: getBgService('organizationService'), deps: [] }, - { provide: ProviderService, useFactory: getBgService('providerService'), deps: [] }, - { provide: 'SECURE_STORAGE', useFactory: getBgService('secureStorageService'), deps: [] }, - ], + StateServiceAbstraction, + LogServiceAbstraction, + ], + multi: true, + }, + LaunchGuardService, + { provide: BaseLockGuardService, useClass: LockGuardService }, + { provide: BaseUnauthGuardService, useClass: UnauthGuardService }, + DebounceNavigationService, + PopupUtilsService, + { provide: MessagingService, useClass: BrowserMessagingService }, + { + provide: AuthServiceAbstraction, + useFactory: getBgService("authService"), + deps: [], + }, + { provide: StateServiceAbstraction, useFactory: getBgService("stateService") }, + { + provide: SearchServiceAbstraction, + useFactory: ( + cipherService: CipherService, + logService: ConsoleLogService, + i18nService: I18nService + ) => { + return isPrivateMode + ? null + : new PopupSearchService( + getBgService("searchService")(), + cipherService, + logService, + i18nService + ); + }, + deps: [CipherService, LogServiceAbstraction, I18nService], + }, + { provide: AuditService, useFactory: getBgService("auditService"), deps: [] }, + { + provide: FileUploadService, + useFactory: getBgService("fileUploadService"), + deps: [], + }, + { provide: CipherService, useFactory: getBgService("cipherService"), deps: [] }, + { + provide: CryptoFunctionService, + useFactory: getBgService("cryptoFunctionService"), + deps: [], + }, + { provide: FolderService, useFactory: getBgService("folderService"), deps: [] }, + { + provide: CollectionService, + useFactory: getBgService("collectionService"), + deps: [], + }, + { + provide: LogServiceAbstraction, + useFactory: getBgService("logService"), + deps: [], + }, + { + provide: EnvironmentService, + useFactory: getBgService("environmentService"), + deps: [], + }, + { provide: TotpService, useFactory: getBgService("totpService"), deps: [] }, + { provide: TokenService, useFactory: getBgService("tokenService"), deps: [] }, + { provide: I18nService, useFactory: getBgService("i18nService"), deps: [] }, + { provide: CryptoService, useFactory: getBgService("cryptoService"), deps: [] }, + { provide: EventService, useFactory: getBgService("eventService"), deps: [] }, + { provide: PolicyService, useFactory: getBgService("policyService"), deps: [] }, + { + provide: PlatformUtilsService, + useFactory: getBgService("platformUtilsService"), + deps: [], + }, + { + provide: PasswordGenerationService, + useFactory: getBgService("passwordGenerationService"), + deps: [], + }, + { provide: ApiService, useFactory: getBgService("apiService"), deps: [] }, + { provide: SyncService, useFactory: getBgService("syncService"), deps: [] }, + { + provide: SettingsService, + useFactory: getBgService("settingsService"), + deps: [], + }, + { + provide: StorageService, + useFactory: getBgService("storageService"), + deps: [], + }, + { provide: AppIdService, useFactory: getBgService("appIdService"), deps: [] }, + { + provide: AutofillService, + useFactory: getBgService("autofillService"), + deps: [], + }, + { provide: ExportService, useFactory: getBgService("exportService"), deps: [] }, + { provide: SendService, useFactory: getBgService("sendService"), deps: [] }, + { + provide: KeyConnectorService, + useFactory: getBgService("keyConnectorService"), + deps: [], + }, + { + provide: UserVerificationService, + useFactory: getBgService("userVerificationService"), + deps: [], + }, + { + provide: VaultTimeoutService, + useFactory: getBgService("vaultTimeoutService"), + deps: [], + }, + { + provide: NotificationsService, + useFactory: getBgService("notificationsService"), + deps: [], + }, + { + provide: LogServiceAbstraction, + useFactory: getBgService("logService"), + deps: [], + }, + { provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService }, + { provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService }, + { + provide: OrganizationService, + useFactory: getBgService("organizationService"), + deps: [], + }, + { + provide: ProviderService, + useFactory: getBgService("providerService"), + deps: [], + }, + { + provide: "SECURE_STORAGE", + useFactory: getBgService("secureStorageService"), + deps: [], + }, + ], }) -export class ServicesModule { -} +export class ServicesModule {} diff --git a/src/popup/settings/excluded-domains.component.ts b/src/popup/settings/excluded-domains.component.ts index 21d1f3d09c..ca9d727898 100644 --- a/src/popup/settings/excluded-domains.component.ts +++ b/src/popup/settings/excluded-domains.component.ts @@ -1,114 +1,118 @@ -import { - Component, - NgZone, - OnDestroy, - OnInit, -} from '@angular/core'; +import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { Router } from '@angular/router'; +import { Router } from "@angular/router"; -import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; -import { BrowserApi } from '../../browser/browserApi'; +import { BrowserApi } from "../../browser/browserApi"; -import { StateService } from '../../services/abstractions/state.service'; +import { StateService } from "../../services/abstractions/state.service"; -import { Utils } from 'jslib-common/misc/utils'; +import { Utils } from "jslib-common/misc/utils"; interface ExcludedDomain { - uri: string; - showCurrentUris: boolean; + uri: string; + showCurrentUris: boolean; } -const BroadcasterSubscriptionId = 'excludedDomains'; +const BroadcasterSubscriptionId = "excludedDomains"; @Component({ - selector: 'app-excluded-domains', - templateUrl: 'excluded-domains.component.html', + selector: "app-excluded-domains", + templateUrl: "excluded-domains.component.html", }) export class ExcludedDomainsComponent implements OnInit, OnDestroy { - excludedDomains: ExcludedDomain[] = []; - currentUris: string[]; - loadCurrentUrisTimeout: number; + excludedDomains: ExcludedDomain[] = []; + currentUris: string[]; + loadCurrentUrisTimeout: number; - constructor(private stateService: StateService, - private i18nService: I18nService, private router: Router, - private broadcasterService: BroadcasterService, private ngZone: NgZone, - private platformUtilsService: PlatformUtilsService) { + constructor( + private stateService: StateService, + private i18nService: I18nService, + private router: Router, + private broadcasterService: BroadcasterService, + private ngZone: NgZone, + private platformUtilsService: PlatformUtilsService + ) {} + + async ngOnInit() { + const savedDomains = await this.stateService.getNeverDomains(); + if (savedDomains) { + for (const uri of Object.keys(savedDomains)) { + this.excludedDomains.push({ uri: uri, showCurrentUris: false }); + } } - async ngOnInit() { - const savedDomains = await this.stateService.getNeverDomains(); - if (savedDomains) { - for (const uri of Object.keys(savedDomains)) { - this.excludedDomains.push({ uri: uri, showCurrentUris: false }); + await this.loadCurrentUris(); + + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case "tabChanged": + case "windowChanged": + if (this.loadCurrentUrisTimeout != null) { + window.clearTimeout(this.loadCurrentUrisTimeout); } + this.loadCurrentUrisTimeout = window.setTimeout( + async () => await this.loadCurrentUris(), + 500 + ); + break; + default: + break; } + }); + }); + } - await this.loadCurrentUris(); + ngOnDestroy() { + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + } - this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case 'tabChanged': - case 'windowChanged': - if (this.loadCurrentUrisTimeout != null) { - window.clearTimeout(this.loadCurrentUrisTimeout); - } - this.loadCurrentUrisTimeout = window.setTimeout(async () => await this.loadCurrentUris(), 500); - break; - default: - break; - } - }); - }); - } + async addUri() { + this.excludedDomains.push({ uri: "", showCurrentUris: false }); + } - ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - } + async removeUri(i: number) { + this.excludedDomains.splice(i, 1); + } - async addUri() { - this.excludedDomains.push({ uri: '', showCurrentUris: false }); - } - - async removeUri(i: number) { - this.excludedDomains.splice(i, 1); - } - - async submit() { - const savedDomains: { [name: string]: null } = {}; - for (const domain of this.excludedDomains) { - if (domain.uri && domain.uri !== '') { - const validDomain = Utils.getHostname(domain.uri); - if (!validDomain) { - this.platformUtilsService.showToast('error', null, - this.i18nService.t('excludedDomainsInvalidDomain', domain.uri)); - return; - } - savedDomains[validDomain] = null; - } + async submit() { + const savedDomains: { [name: string]: null } = {}; + for (const domain of this.excludedDomains) { + if (domain.uri && domain.uri !== "") { + const validDomain = Utils.getHostname(domain.uri); + if (!validDomain) { + this.platformUtilsService.showToast( + "error", + null, + this.i18nService.t("excludedDomainsInvalidDomain", domain.uri) + ); + return; } - await this.stateService.setNeverDomains(savedDomains); - this.router.navigate(['/tabs/settings']); + savedDomains[validDomain] = null; + } } + await this.stateService.setNeverDomains(savedDomains); + this.router.navigate(["/tabs/settings"]); + } - trackByFunction(index: number, item: any) { - return index; - } + trackByFunction(index: number, item: any) { + return index; + } - toggleUriInput(domain: ExcludedDomain) { - domain.showCurrentUris = !domain.showCurrentUris; - } + toggleUriInput(domain: ExcludedDomain) { + domain.showCurrentUris = !domain.showCurrentUris; + } - async loadCurrentUris() { - const tabs = await BrowserApi.tabsQuery({ windowType: 'normal' }); - if (tabs) { - const uriSet = new Set(tabs.map(tab => Utils.getHostname(tab.url))); - uriSet.delete(null); - this.currentUris = Array.from(uriSet); - } + async loadCurrentUris() { + const tabs = await BrowserApi.tabsQuery({ windowType: "normal" }); + if (tabs) { + const uriSet = new Set(tabs.map((tab) => Utils.getHostname(tab.url))); + uriSet.delete(null); + this.currentUris = Array.from(uriSet); } + } } diff --git a/src/popup/settings/options.component.ts b/src/popup/settings/options.component.ts index 9514ae6b77..c8c5de2187 100644 --- a/src/popup/settings/options.component.ts +++ b/src/popup/settings/options.component.ts @@ -1,156 +1,161 @@ -import { - Component, - OnInit, -} from '@angular/core'; +import { Component, OnInit } from "@angular/core"; -import { ThemeType } from 'jslib-common/enums/themeType'; -import { UriMatchType } from 'jslib-common/enums/uriMatchType'; +import { ThemeType } from "jslib-common/enums/themeType"; +import { UriMatchType } from "jslib-common/enums/uriMatchType"; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { TotpService } from 'jslib-common/abstractions/totp.service'; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { TotpService } from "jslib-common/abstractions/totp.service"; -import { StateService } from '../../services/abstractions/state.service'; +import { StateService } from "../../services/abstractions/state.service"; @Component({ - selector: 'app-options', - templateUrl: 'options.component.html', + selector: "app-options", + templateUrl: "options.component.html", }) export class OptionsComponent implements OnInit { - disableFavicon = false; - disableBadgeCounter = false; - enableAutoFillOnPageLoad = false; - autoFillOnPageLoadDefault = false; - autoFillOnPageLoadOptions: any[]; - disableAutoTotpCopy = false; - disableContextMenuItem = false; - disableAddLoginNotification = false; - disableChangedPasswordNotification = false; - dontShowCards = false; - dontShowIdentities = false; - showClearClipboard = true; - theme: string; - themeOptions: any[]; - defaultUriMatch = UriMatchType.Domain; - uriMatchOptions: any[]; - clearClipboard: number; - clearClipboardOptions: any[]; - showGeneral: boolean = true; - showAutofill: boolean = true; - showDisplay: boolean = true; + disableFavicon = false; + disableBadgeCounter = false; + enableAutoFillOnPageLoad = false; + autoFillOnPageLoadDefault = false; + autoFillOnPageLoadOptions: any[]; + disableAutoTotpCopy = false; + disableContextMenuItem = false; + disableAddLoginNotification = false; + disableChangedPasswordNotification = false; + dontShowCards = false; + dontShowIdentities = false; + showClearClipboard = true; + theme: string; + themeOptions: any[]; + defaultUriMatch = UriMatchType.Domain; + uriMatchOptions: any[]; + clearClipboard: number; + clearClipboardOptions: any[]; + showGeneral: boolean = true; + showAutofill: boolean = true; + showDisplay: boolean = true; - constructor(private messagingService: MessagingService, private stateService: StateService, - private totpService: TotpService, i18nService: I18nService) { - this.themeOptions = [ - { name: i18nService.t('default'), value: null }, - { name: i18nService.t('light'), value: ThemeType.Light }, - { name: i18nService.t('dark'), value: ThemeType.Dark }, - { name: 'Nord', value: ThemeType.Nord }, - { name: i18nService.t('solarizedDark'), value: ThemeType.SolarizedDark }, - ]; - this.uriMatchOptions = [ - { name: i18nService.t('baseDomain'), value: UriMatchType.Domain }, - { name: i18nService.t('host'), value: UriMatchType.Host }, - { name: i18nService.t('startsWith'), value: UriMatchType.StartsWith }, - { name: i18nService.t('regEx'), value: UriMatchType.RegularExpression }, - { name: i18nService.t('exact'), value: UriMatchType.Exact }, - { name: i18nService.t('never'), value: UriMatchType.Never }, - ]; - this.clearClipboardOptions = [ - { name: i18nService.t('never'), value: null }, - { name: i18nService.t('tenSeconds'), value: 10 }, - { name: i18nService.t('twentySeconds'), value: 20 }, - { name: i18nService.t('thirtySeconds'), value: 30 }, - { name: i18nService.t('oneMinute'), value: 60 }, - { name: i18nService.t('twoMinutes'), value: 120 }, - { name: i18nService.t('fiveMinutes'), value: 300 }, - ]; - this.autoFillOnPageLoadOptions = [ - { name: i18nService.t('autoFillOnPageLoadYes'), value: true }, - { name: i18nService.t('autoFillOnPageLoadNo'), value: false }, - ]; - } + constructor( + private messagingService: MessagingService, + private stateService: StateService, + private totpService: TotpService, + i18nService: I18nService + ) { + this.themeOptions = [ + { name: i18nService.t("default"), value: null }, + { name: i18nService.t("light"), value: ThemeType.Light }, + { name: i18nService.t("dark"), value: ThemeType.Dark }, + { name: "Nord", value: ThemeType.Nord }, + { name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark }, + ]; + this.uriMatchOptions = [ + { name: i18nService.t("baseDomain"), value: UriMatchType.Domain }, + { name: i18nService.t("host"), value: UriMatchType.Host }, + { name: i18nService.t("startsWith"), value: UriMatchType.StartsWith }, + { name: i18nService.t("regEx"), value: UriMatchType.RegularExpression }, + { name: i18nService.t("exact"), value: UriMatchType.Exact }, + { name: i18nService.t("never"), value: UriMatchType.Never }, + ]; + this.clearClipboardOptions = [ + { name: i18nService.t("never"), value: null }, + { name: i18nService.t("tenSeconds"), value: 10 }, + { name: i18nService.t("twentySeconds"), value: 20 }, + { name: i18nService.t("thirtySeconds"), value: 30 }, + { name: i18nService.t("oneMinute"), value: 60 }, + { name: i18nService.t("twoMinutes"), value: 120 }, + { name: i18nService.t("fiveMinutes"), value: 300 }, + ]; + this.autoFillOnPageLoadOptions = [ + { name: i18nService.t("autoFillOnPageLoadYes"), value: true }, + { name: i18nService.t("autoFillOnPageLoadNo"), value: false }, + ]; + } - async ngOnInit() { - this.enableAutoFillOnPageLoad = await this.stateService.getEnableAutoFillOnPageLoad(); + async ngOnInit() { + this.enableAutoFillOnPageLoad = await this.stateService.getEnableAutoFillOnPageLoad(); - this.autoFillOnPageLoadDefault = await this.stateService.getAutoFillOnPageLoadDefault() ?? true; + this.autoFillOnPageLoadDefault = + (await this.stateService.getAutoFillOnPageLoadDefault()) ?? true; - this.disableAddLoginNotification = await this.stateService.getDisableAddLoginNotification(); + this.disableAddLoginNotification = await this.stateService.getDisableAddLoginNotification(); - this.disableChangedPasswordNotification = await this.stateService.getDisableChangedPasswordNotification(); + this.disableChangedPasswordNotification = + await this.stateService.getDisableChangedPasswordNotification(); - this.disableContextMenuItem = await this.stateService.getDisableContextMenuItem(); + this.disableContextMenuItem = await this.stateService.getDisableContextMenuItem(); - this.dontShowCards = await this.stateService.getDontShowCardsCurrentTab(); - this.dontShowIdentities = await this.stateService.getDontShowIdentitiesCurrentTab(); + this.dontShowCards = await this.stateService.getDontShowCardsCurrentTab(); + this.dontShowIdentities = await this.stateService.getDontShowIdentitiesCurrentTab(); - this.disableAutoTotpCopy = !(await this.totpService.isAutoCopyEnabled()); + this.disableAutoTotpCopy = !(await this.totpService.isAutoCopyEnabled()); - this.disableFavicon = await this.stateService.getDisableFavicon(); + this.disableFavicon = await this.stateService.getDisableFavicon(); - this.disableBadgeCounter = await this.stateService.getDisableBadgeCounter(); + this.disableBadgeCounter = await this.stateService.getDisableBadgeCounter(); - this.theme = await this.stateService.getTheme(); + this.theme = await this.stateService.getTheme(); - const defaultUriMatch = await this.stateService.getDefaultUriMatch(); - this.defaultUriMatch = defaultUriMatch == null ? UriMatchType.Domain : defaultUriMatch; + const defaultUriMatch = await this.stateService.getDefaultUriMatch(); + this.defaultUriMatch = defaultUriMatch == null ? UriMatchType.Domain : defaultUriMatch; - this.clearClipboard = await this.stateService.getClearClipboard(); - } + this.clearClipboard = await this.stateService.getClearClipboard(); + } - async updateAddLoginNotification() { - await this.stateService.setDisableAddLoginNotification(this.disableAddLoginNotification); - } + async updateAddLoginNotification() { + await this.stateService.setDisableAddLoginNotification(this.disableAddLoginNotification); + } - async updateChangedPasswordNotification() { - await this.stateService.setDisableChangedPasswordNotification(this.disableChangedPasswordNotification); - } + async updateChangedPasswordNotification() { + await this.stateService.setDisableChangedPasswordNotification( + this.disableChangedPasswordNotification + ); + } - async updateDisableContextMenuItem() { - await this.stateService.setDisableContextMenuItem(this.disableContextMenuItem); - this.messagingService.send('bgUpdateContextMenu'); - } + async updateDisableContextMenuItem() { + await this.stateService.setDisableContextMenuItem(this.disableContextMenuItem); + this.messagingService.send("bgUpdateContextMenu"); + } - async updateAutoTotpCopy() { - await this.stateService.setDisableAutoTotpCopy(this.disableAutoTotpCopy); - } + async updateAutoTotpCopy() { + await this.stateService.setDisableAutoTotpCopy(this.disableAutoTotpCopy); + } - async updateAutoFillOnPageLoad() { - await this.stateService.setEnableAutoFillOnPageLoad(this.enableAutoFillOnPageLoad); - } + async updateAutoFillOnPageLoad() { + await this.stateService.setEnableAutoFillOnPageLoad(this.enableAutoFillOnPageLoad); + } - async updateAutoFillOnPageLoadDefault() { - await this.stateService.setAutoFillOnPageLoadDefault(this.autoFillOnPageLoadDefault); - } + async updateAutoFillOnPageLoadDefault() { + await this.stateService.setAutoFillOnPageLoadDefault(this.autoFillOnPageLoadDefault); + } - async updateDisableFavicon() { - await this.stateService.setDisableFavicon(this.disableFavicon); - } + async updateDisableFavicon() { + await this.stateService.setDisableFavicon(this.disableFavicon); + } - async updateDisableBadgeCounter() { - await this.stateService.setDisableBadgeCounter(this.disableBadgeCounter); - this.messagingService.send('bgUpdateContextMenu'); - } + async updateDisableBadgeCounter() { + await this.stateService.setDisableBadgeCounter(this.disableBadgeCounter); + this.messagingService.send("bgUpdateContextMenu"); + } - async updateShowCards() { - await this.stateService.setDontShowCardsCurrentTab(this.dontShowCards); - } + async updateShowCards() { + await this.stateService.setDontShowCardsCurrentTab(this.dontShowCards); + } - async updateShowIdentities() { - await this.stateService.setDontShowIdentitiesCurrentTab(this.dontShowIdentities); - } + async updateShowIdentities() { + await this.stateService.setDontShowIdentitiesCurrentTab(this.dontShowIdentities); + } - async saveTheme() { - await this.stateService.setTheme(this.theme); - window.setTimeout(() => window.location.reload(), 200); - } + async saveTheme() { + await this.stateService.setTheme(this.theme); + window.setTimeout(() => window.location.reload(), 200); + } - async saveDefaultUriMatch() { - await this.stateService.setDefaultUriMatch(this.defaultUriMatch); - } + async saveDefaultUriMatch() { + await this.stateService.setDefaultUriMatch(this.defaultUriMatch); + } - async saveClearClipboard() { - await this.stateService.setClearClipboard(this.clearClipboard); - } + async saveClearClipboard() { + await this.stateService.setClearClipboard(this.clearClipboard); + } } diff --git a/src/popup/settings/premium.component.ts b/src/popup/settings/premium.component.ts index c87b2e74b4..f27c0a4bc5 100644 --- a/src/popup/settings/premium.component.ts +++ b/src/popup/settings/premium.component.ts @@ -1,31 +1,36 @@ -import { CurrencyPipe } from '@angular/common'; -import { Component } from '@angular/core'; +import { CurrencyPipe } from "@angular/common"; +import { Component } from "@angular/core"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; -import { PremiumComponent as BasePremiumComponent } from 'jslib-angular/components/premium.component'; -import { StateService } from '../../services/abstractions/state.service'; +import { PremiumComponent as BasePremiumComponent } from "jslib-angular/components/premium.component"; +import { StateService } from "../../services/abstractions/state.service"; @Component({ - selector: 'app-premium', - templateUrl: 'premium.component.html', + selector: "app-premium", + templateUrl: "premium.component.html", }) export class PremiumComponent extends BasePremiumComponent { - priceString: string; + priceString: string; - constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService, - apiService: ApiService, logService: LogService, stateService: StateService, - private currencyPipe: CurrencyPipe) { - super(i18nService, platformUtilsService, apiService, logService, stateService); + constructor( + i18nService: I18nService, + platformUtilsService: PlatformUtilsService, + apiService: ApiService, + logService: LogService, + stateService: StateService, + private currencyPipe: CurrencyPipe + ) { + super(i18nService, platformUtilsService, apiService, logService, stateService); - // Support old price string. Can be removed in future once all translations are properly updated. - const thePrice = this.currencyPipe.transform(this.price, '$'); - this.priceString = i18nService.t('premiumPrice', thePrice); - if (this.priceString.indexOf('%price%') > -1) { - this.priceString = this.priceString.replace('%price%', thePrice); - } + // Support old price string. Can be removed in future once all translations are properly updated. + const thePrice = this.currencyPipe.transform(this.price, "$"); + this.priceString = i18nService.t("premiumPrice", thePrice); + if (this.priceString.indexOf("%price%") > -1) { + this.priceString = this.priceString.replace("%price%", thePrice); } + } } diff --git a/src/popup/settings/settings.component.ts b/src/popup/settings/settings.component.ts index 2ee7275df6..47458f257d 100644 --- a/src/popup/settings/settings.component.ts +++ b/src/popup/settings/settings.component.ts @@ -1,364 +1,410 @@ -import { - Component, - ElementRef, - OnInit, - ViewChild, -} from '@angular/core'; -import { FormControl } from '@angular/forms'; -import { Router } from '@angular/router'; -import Swal from 'sweetalert2/src/sweetalert2.js'; +import { Component, ElementRef, OnInit, ViewChild } from "@angular/core"; +import { FormControl } from "@angular/forms"; +import { Router } from "@angular/router"; +import Swal from "sweetalert2/src/sweetalert2.js"; -import { BrowserApi } from '../../browser/browserApi'; +import { BrowserApi } from "../../browser/browserApi"; -import { DeviceType } from 'jslib-common/enums/deviceType'; +import { DeviceType } from "jslib-common/enums/deviceType"; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; -import { PopupUtilsService } from '../services/popup-utils.service'; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; +import { PopupUtilsService } from "../services/popup-utils.service"; -import { ModalService } from 'jslib-angular/services/modal.service'; +import { ModalService } from "jslib-angular/services/modal.service"; -import { StateService } from '../../services/abstractions/state.service'; -import { SetPinComponent } from '../components/set-pin.component'; +import { StateService } from "../../services/abstractions/state.service"; +import { SetPinComponent } from "../components/set-pin.component"; const RateUrls = { - [DeviceType.ChromeExtension]: - 'https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews', - [DeviceType.FirefoxExtension]: - 'https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/#reviews', - [DeviceType.OperaExtension]: - 'https://addons.opera.com/en/extensions/details/bitwarden-free-password-manager/#feedback-container', - [DeviceType.EdgeExtension]: - 'https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh', - [DeviceType.VivaldiExtension]: - 'https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews', - [DeviceType.SafariExtension]: - 'https://apps.apple.com/app/bitwarden/id1352778147', + [DeviceType.ChromeExtension]: + "https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews", + [DeviceType.FirefoxExtension]: + "https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/#reviews", + [DeviceType.OperaExtension]: + "https://addons.opera.com/en/extensions/details/bitwarden-free-password-manager/#feedback-container", + [DeviceType.EdgeExtension]: + "https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh", + [DeviceType.VivaldiExtension]: + "https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews", + [DeviceType.SafariExtension]: "https://apps.apple.com/app/bitwarden/id1352778147", }; @Component({ - selector: 'app-settings', - templateUrl: 'settings.component.html', + selector: "app-settings", + templateUrl: "settings.component.html", }) export class SettingsComponent implements OnInit { - @ViewChild('vaultTimeoutActionSelect', { read: ElementRef, static: true }) vaultTimeoutActionSelectRef: ElementRef; - vaultTimeouts: any[]; - vaultTimeoutActions: any[]; - vaultTimeoutAction: string; - pin: boolean = null; - supportsBiometric: boolean; - biometric: boolean = false; - disableAutoBiometricsPrompt = true; - previousVaultTimeout: number = null; - showChangeMasterPass = true; + @ViewChild("vaultTimeoutActionSelect", { read: ElementRef, static: true }) + vaultTimeoutActionSelectRef: ElementRef; + vaultTimeouts: any[]; + vaultTimeoutActions: any[]; + vaultTimeoutAction: string; + pin: boolean = null; + supportsBiometric: boolean; + biometric: boolean = false; + disableAutoBiometricsPrompt = true; + previousVaultTimeout: number = null; + showChangeMasterPass = true; - vaultTimeout: FormControl = new FormControl(null); + vaultTimeout: FormControl = new FormControl(null); - constructor(private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, - private vaultTimeoutService: VaultTimeoutService, - public messagingService: MessagingService, private router: Router, - private environmentService: EnvironmentService, private cryptoService: CryptoService, - private stateService: StateService, private popupUtilsService: PopupUtilsService, - private modalService: ModalService, - private keyConnectorService: KeyConnectorService) { + constructor( + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + private vaultTimeoutService: VaultTimeoutService, + public messagingService: MessagingService, + private router: Router, + private environmentService: EnvironmentService, + private cryptoService: CryptoService, + private stateService: StateService, + private popupUtilsService: PopupUtilsService, + private modalService: ModalService, + private keyConnectorService: KeyConnectorService + ) {} + + async ngOnInit() { + const showOnLocked = + !this.platformUtilsService.isFirefox() && !this.platformUtilsService.isSafari(); + + this.vaultTimeouts = [ + { name: this.i18nService.t("immediately"), value: 0 }, + { name: this.i18nService.t("oneMinute"), value: 1 }, + { name: this.i18nService.t("fiveMinutes"), value: 5 }, + { name: this.i18nService.t("fifteenMinutes"), value: 15 }, + { name: this.i18nService.t("thirtyMinutes"), value: 30 }, + { name: this.i18nService.t("oneHour"), value: 60 }, + { name: this.i18nService.t("fourHours"), value: 240 }, + // { name: i18nService.t('onIdle'), value: -4 }, + // { name: i18nService.t('onSleep'), value: -3 }, + ]; + + if (showOnLocked) { + this.vaultTimeouts.push({ name: this.i18nService.t("onLocked"), value: -2 }); } - async ngOnInit() { - const showOnLocked = !this.platformUtilsService.isFirefox() && !this.platformUtilsService.isSafari(); + this.vaultTimeouts.push({ name: this.i18nService.t("onRestart"), value: -1 }); + this.vaultTimeouts.push({ name: this.i18nService.t("never"), value: null }); - this.vaultTimeouts = [ - { name: this.i18nService.t('immediately'), value: 0 }, - { name: this.i18nService.t('oneMinute'), value: 1 }, - { name: this.i18nService.t('fiveMinutes'), value: 5 }, - { name: this.i18nService.t('fifteenMinutes'), value: 15 }, - { name: this.i18nService.t('thirtyMinutes'), value: 30 }, - { name: this.i18nService.t('oneHour'), value: 60 }, - { name: this.i18nService.t('fourHours'), value: 240 }, - // { name: i18nService.t('onIdle'), value: -4 }, - // { name: i18nService.t('onSleep'), value: -3 }, - ]; + this.vaultTimeoutActions = [ + { name: this.i18nService.t("lock"), value: "lock" }, + { name: this.i18nService.t("logOut"), value: "logOut" }, + ]; - if (showOnLocked) { - this.vaultTimeouts.push({ name: this.i18nService.t('onLocked'), value: -2 }); - } + let timeout = await this.vaultTimeoutService.getVaultTimeout(); + if (timeout != null) { + if (timeout === -2 && !showOnLocked) { + timeout = -1; + } + this.vaultTimeout.setValue(timeout); + } + this.previousVaultTimeout = this.vaultTimeout.value; + this.vaultTimeout.valueChanges.subscribe((value) => { + this.saveVaultTimeout(value); + }); - this.vaultTimeouts.push({ name: this.i18nService.t('onRestart'), value: -1 }); - this.vaultTimeouts.push({ name: this.i18nService.t('never'), value: null }); + const action = await this.stateService.getVaultTimeoutAction(); + this.vaultTimeoutAction = action == null ? "lock" : action; - this.vaultTimeoutActions = [ - { name: this.i18nService.t('lock'), value: 'lock' }, - { name: this.i18nService.t('logOut'), value: 'logOut' }, - ]; + const pinSet = await this.vaultTimeoutService.isPinLockSet(); + this.pin = pinSet[0] || pinSet[1]; - let timeout = await this.vaultTimeoutService.getVaultTimeout(); - if (timeout != null) { - if (timeout === -2 && !showOnLocked) { - timeout = -1; - } - this.vaultTimeout.setValue(timeout); - } - this.previousVaultTimeout = this.vaultTimeout.value; - this.vaultTimeout.valueChanges.subscribe(value => { - this.saveVaultTimeout(value); + this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); + this.biometric = await this.vaultTimeoutService.isBiometricLockSet(); + this.disableAutoBiometricsPrompt = + (await this.stateService.getDisableAutoBiometricsPrompt()) ?? true; + this.showChangeMasterPass = !(await this.keyConnectorService.getUsesKeyConnector()); + } + + async saveVaultTimeout(newValue: number) { + if (newValue == null) { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("neverLockWarning"), + null, + this.i18nService.t("yes"), + this.i18nService.t("cancel"), + "warning" + ); + if (!confirmed) { + this.vaultTimeout.setValue(this.previousVaultTimeout); + return; + } + } + + if (!this.vaultTimeout.valid) { + this.platformUtilsService.showToast("error", null, this.i18nService.t("vaultTimeoutToLarge")); + return; + } + + this.previousVaultTimeout = this.vaultTimeout.value; + + await this.vaultTimeoutService.setVaultTimeoutOptions( + this.vaultTimeout.value, + this.vaultTimeoutAction + ); + if (this.previousVaultTimeout == null) { + this.messagingService.send("bgReseedStorage"); + } + } + + async saveVaultTimeoutAction(newValue: string) { + if (newValue === "logOut") { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("vaultTimeoutLogOutConfirmation"), + this.i18nService.t("vaultTimeoutLogOutConfirmationTitle"), + this.i18nService.t("yes"), + this.i18nService.t("cancel"), + "warning" + ); + if (!confirmed) { + this.vaultTimeoutActions.forEach((option: any, i) => { + if (option.value === this.vaultTimeoutAction) { + this.vaultTimeoutActionSelectRef.nativeElement.value = + i + ": " + this.vaultTimeoutAction; + } }); - - const action = await this.stateService.getVaultTimeoutAction(); - this.vaultTimeoutAction = action == null ? 'lock' : action; - - const pinSet = await this.vaultTimeoutService.isPinLockSet(); - this.pin = pinSet[0] || pinSet[1]; - - this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); - this.biometric = await this.vaultTimeoutService.isBiometricLockSet(); - this.disableAutoBiometricsPrompt = await this.stateService.getDisableAutoBiometricsPrompt() ?? true; - this.showChangeMasterPass = !await this.keyConnectorService.getUsesKeyConnector(); + return; + } } - async saveVaultTimeout(newValue: number) { - if (newValue == null) { - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('neverLockWarning'), null, - this.i18nService.t('yes'), this.i18nService.t('cancel'), 'warning'); - if (!confirmed) { - this.vaultTimeout.setValue(this.previousVaultTimeout); - return; + if (!this.vaultTimeout.valid) { + this.platformUtilsService.showToast("error", null, this.i18nService.t("vaultTimeoutToLarge")); + return; + } + + this.vaultTimeoutAction = newValue; + await this.vaultTimeoutService.setVaultTimeoutOptions( + this.vaultTimeout.value, + this.vaultTimeoutAction + ); + } + + async updatePin() { + if (this.pin) { + const ref = this.modalService.open(SetPinComponent, { allowMultipleModals: true }); + + if (ref == null) { + this.pin = false; + return; + } + + this.pin = await ref.onClosedPromise(); + } else { + await this.cryptoService.clearPinProtectedKey(); + await this.vaultTimeoutService.clear(); + } + } + + async updateBiometric() { + if (this.biometric && this.supportsBiometric) { + let granted; + try { + granted = await BrowserApi.requestPermission({ permissions: ["nativeMessaging"] }); + } catch (e) { + // tslint:disable-next-line + console.error(e); + + if (this.platformUtilsService.isFirefox() && this.popupUtilsService.inSidebar(window)) { + await this.platformUtilsService.showDialog( + this.i18nService.t("nativeMessaginPermissionSidebarDesc"), + this.i18nService.t("nativeMessaginPermissionSidebarTitle"), + this.i18nService.t("ok"), + null + ); + this.biometric = false; + return; + } + } + + if (!granted) { + await this.platformUtilsService.showDialog( + this.i18nService.t("nativeMessaginPermissionErrorDesc"), + this.i18nService.t("nativeMessaginPermissionErrorTitle"), + this.i18nService.t("ok"), + null + ); + this.biometric = false; + return; + } + + const submitted = Swal.fire({ + heightAuto: false, + buttonsStyling: false, + titleText: this.i18nService.t("awaitDesktop"), + text: this.i18nService.t("awaitDesktopDesc"), + icon: "info", + iconHtml: '', + showCancelButton: true, + cancelButtonText: this.i18nService.t("cancel"), + showConfirmButton: false, + allowOutsideClick: false, + }); + + await this.stateService.setBiometricAwaitingAcceptance(true); + await this.cryptoService.toggleKey(); + + await Promise.race([ + submitted.then(async (result) => { + if (result.dismiss === Swal.DismissReason.cancel) { + this.biometric = false; + await this.stateService.setBiometricAwaitingAcceptance(null); + } + }), + this.platformUtilsService + .authenticateBiometric() + .then((result) => { + this.biometric = result; + + Swal.close(); + if (this.biometric === false) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorEnableBiometricTitle"), + this.i18nService.t("errorEnableBiometricDesc") + ); } - } - - if (!this.vaultTimeout.valid) { - this.platformUtilsService.showToast('error', null, this.i18nService.t('vaultTimeoutToLarge')); - return; - } - - this.previousVaultTimeout = this.vaultTimeout.value; - - await this.vaultTimeoutService.setVaultTimeoutOptions(this.vaultTimeout.value, this.vaultTimeoutAction); - if (this.previousVaultTimeout == null) { - this.messagingService.send('bgReseedStorage'); - } + }) + .catch((e) => { + // Handle connection errors + this.biometric = false; + }), + ]); + } else { + await this.stateService.setBiometricUnlock(null); + await this.stateService.setBiometricLocked(false); } + } - async saveVaultTimeoutAction(newValue: string) { - if (newValue === 'logOut') { - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('vaultTimeoutLogOutConfirmation'), - this.i18nService.t('vaultTimeoutLogOutConfirmationTitle'), - this.i18nService.t('yes'), this.i18nService.t('cancel'), 'warning'); - if (!confirmed) { - this.vaultTimeoutActions.forEach((option: any, i) => { - if (option.value === this.vaultTimeoutAction) { - this.vaultTimeoutActionSelectRef.nativeElement.value = i + ': ' + this.vaultTimeoutAction; - } - }); - return; - } - } + async updateAutoBiometricsPrompt() { + await this.stateService.setDisableAutoBiometricsPrompt(this.disableAutoBiometricsPrompt); + } - if (!this.vaultTimeout.valid) { - this.platformUtilsService.showToast('error', null, this.i18nService.t('vaultTimeoutToLarge')); - return; - } + async lock() { + await this.vaultTimeoutService.lock(true); + } - this.vaultTimeoutAction = newValue; - await this.vaultTimeoutService.setVaultTimeoutOptions(this.vaultTimeout.value, this.vaultTimeoutAction); + async logOut() { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("logOutConfirmation"), + this.i18nService.t("logOut"), + this.i18nService.t("yes"), + this.i18nService.t("cancel") + ); + if (confirmed) { + this.messagingService.send("logout"); } + } - async updatePin() { - if (this.pin) { - const ref = this.modalService.open(SetPinComponent, { allowMultipleModals: true }); - - if (ref == null) { - this.pin = false; - return; - } - - this.pin = await ref.onClosedPromise(); - } else { - await this.cryptoService.clearPinProtectedKey(); - await this.vaultTimeoutService.clear(); - } + async changePassword() { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("changeMasterPasswordConfirmation"), + this.i18nService.t("changeMasterPassword"), + this.i18nService.t("yes"), + this.i18nService.t("cancel") + ); + if (confirmed) { + BrowserApi.createNewTab("https://help.bitwarden.com/article/change-your-master-password/"); } + } - async updateBiometric() { - if (this.biometric && this.supportsBiometric) { - - let granted; - try { - granted = await BrowserApi.requestPermission({ permissions: ['nativeMessaging'] }); - } catch (e) { - // tslint:disable-next-line - console.error(e); - - if (this.platformUtilsService.isFirefox() && this.popupUtilsService.inSidebar(window)) { - await this.platformUtilsService.showDialog( - this.i18nService.t('nativeMessaginPermissionSidebarDesc'), this.i18nService.t('nativeMessaginPermissionSidebarTitle'), - this.i18nService.t('ok'), null); - this.biometric = false; - return; - } - } - - if (!granted) { - await this.platformUtilsService.showDialog( - this.i18nService.t('nativeMessaginPermissionErrorDesc'), this.i18nService.t('nativeMessaginPermissionErrorTitle'), - this.i18nService.t('ok'), null); - this.biometric = false; - return; - } - - const submitted = Swal.fire({ - heightAuto: false, - buttonsStyling: false, - titleText: this.i18nService.t('awaitDesktop'), - text: this.i18nService.t('awaitDesktopDesc'), - icon: 'info', - iconHtml: '', - showCancelButton: true, - cancelButtonText: this.i18nService.t('cancel'), - showConfirmButton: false, - allowOutsideClick: false, - }); - - await this.stateService.setBiometricAwaitingAcceptance(true); - await this.cryptoService.toggleKey(); - - await Promise.race([ - submitted.then(async result => { - if (result.dismiss === Swal.DismissReason.cancel) { - this.biometric = false; - await this.stateService.setBiometricAwaitingAcceptance(null); - } - }), - this.platformUtilsService.authenticateBiometric().then(result => { - this.biometric = result; - - Swal.close(); - if (this.biometric === false) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorEnableBiometricTitle'), this.i18nService.t('errorEnableBiometricDesc')); - } - }).catch(e => { - // Handle connection errors - this.biometric = false; - }), - ]); - } else { - await this.stateService.setBiometricUnlock(null); - await this.stateService.setBiometricLocked(false); - } + async twoStep() { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("twoStepLoginConfirmation"), + this.i18nService.t("twoStepLogin"), + this.i18nService.t("yes"), + this.i18nService.t("cancel") + ); + if (confirmed) { + BrowserApi.createNewTab("https://help.bitwarden.com/article/setup-two-step-login/"); } + } - async updateAutoBiometricsPrompt() { - await this.stateService.setDisableAutoBiometricsPrompt(this.disableAutoBiometricsPrompt); + async share() { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("learnOrgConfirmation"), + this.i18nService.t("learnOrg"), + this.i18nService.t("yes"), + this.i18nService.t("cancel") + ); + if (confirmed) { + BrowserApi.createNewTab("https://help.bitwarden.com/article/what-is-an-organization/"); } + } - async lock() { - await this.vaultTimeoutService.lock(true); + async webVault() { + const url = this.environmentService.getWebVaultUrl(); + BrowserApi.createNewTab(url); + } + + import() { + BrowserApi.createNewTab("https://help.bitwarden.com/article/import-data/"); + } + + export() { + this.router.navigate(["/export"]); + } + + help() { + BrowserApi.createNewTab("https://help.bitwarden.com/"); + } + + about() { + const year = new Date().getFullYear(); + const versionText = document.createTextNode( + this.i18nService.t("version") + ": " + BrowserApi.getApplicationVersion() + ); + const div = document.createElement("div"); + div.innerHTML = + `

+

Bitwarden
© Bitwarden Inc. 2015-` + + year + + `

`; + div.appendChild(versionText); + + Swal.fire({ + heightAuto: false, + buttonsStyling: false, + html: div, + showConfirmButton: false, + showCancelButton: true, + cancelButtonText: this.i18nService.t("close"), + }); + } + + async fingerprint() { + const fingerprint = await this.cryptoService.getFingerprint( + await this.stateService.getUserId() + ); + const p = document.createElement("p"); + p.innerText = this.i18nService.t("yourAccountsFingerprint") + ":"; + const p2 = document.createElement("p"); + p2.innerText = fingerprint.join("-"); + const div = document.createElement("div"); + div.appendChild(p); + div.appendChild(p2); + + const result = await Swal.fire({ + heightAuto: false, + buttonsStyling: false, + html: div, + showCancelButton: true, + cancelButtonText: this.i18nService.t("close"), + showConfirmButton: true, + confirmButtonText: this.i18nService.t("learnMore"), + }); + + if (result.value) { + this.platformUtilsService.launchUri("https://help.bitwarden.com/article/fingerprint-phrase/"); } + } - async logOut() { - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('logOutConfirmation'), this.i18nService.t('logOut'), - this.i18nService.t('yes'), this.i18nService.t('cancel')); - if (confirmed) { - this.messagingService.send('logout'); - } - } - - async changePassword() { - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('changeMasterPasswordConfirmation'), this.i18nService.t('changeMasterPassword'), - this.i18nService.t('yes'), this.i18nService.t('cancel')); - if (confirmed) { - BrowserApi.createNewTab('https://help.bitwarden.com/article/change-your-master-password/'); - } - } - - async twoStep() { - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('twoStepLoginConfirmation'), this.i18nService.t('twoStepLogin'), - this.i18nService.t('yes'), this.i18nService.t('cancel')); - if (confirmed) { - BrowserApi.createNewTab('https://help.bitwarden.com/article/setup-two-step-login/'); - } - } - - async share() { - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('learnOrgConfirmation'), this.i18nService.t('learnOrg'), - this.i18nService.t('yes'), this.i18nService.t('cancel')); - if (confirmed) { - BrowserApi.createNewTab('https://help.bitwarden.com/article/what-is-an-organization/'); - } - } - - async webVault() { - const url = this.environmentService.getWebVaultUrl(); - BrowserApi.createNewTab(url); - } - - import() { - BrowserApi.createNewTab('https://help.bitwarden.com/article/import-data/'); - } - - export() { - this.router.navigate(['/export']); - } - - help() { - BrowserApi.createNewTab('https://help.bitwarden.com/'); - } - - about() { - const year = (new Date()).getFullYear(); - const versionText = document.createTextNode( - this.i18nService.t('version') + ': ' + BrowserApi.getApplicationVersion()); - const div = document.createElement('div'); - div.innerHTML = `

-

Bitwarden
© Bitwarden Inc. 2015-` + year + `

`; - div.appendChild(versionText); - - Swal.fire({ - heightAuto: false, - buttonsStyling: false, - html: div, - showConfirmButton: false, - showCancelButton: true, - cancelButtonText: this.i18nService.t('close'), - }); - } - - async fingerprint() { - const fingerprint = await this.cryptoService.getFingerprint(await this.stateService.getUserId()); - const p = document.createElement('p'); - p.innerText = this.i18nService.t('yourAccountsFingerprint') + ':'; - const p2 = document.createElement('p'); - p2.innerText = fingerprint.join('-'); - const div = document.createElement('div'); - div.appendChild(p); - div.appendChild(p2); - - const result = await Swal.fire({ - heightAuto: false, - buttonsStyling: false, - html: div, - showCancelButton: true, - cancelButtonText: this.i18nService.t('close'), - showConfirmButton: true, - confirmButtonText: this.i18nService.t('learnMore'), - }); - - if (result.value) { - this.platformUtilsService.launchUri('https://help.bitwarden.com/article/fingerprint-phrase/'); - } - } - - rate() { - const deviceType = this.platformUtilsService.getDevice(); - BrowserApi.createNewTab((RateUrls as any)[deviceType]); - } + rate() { + const deviceType = this.platformUtilsService.getDevice(); + BrowserApi.createNewTab((RateUrls as any)[deviceType]); + } } diff --git a/src/popup/vault/add-edit.component.ts b/src/popup/vault/add-edit.component.ts index 27ea36c0c1..f7bc546a15 100644 --- a/src/popup/vault/add-edit.component.ts +++ b/src/popup/vault/add-edit.component.ts @@ -1,190 +1,225 @@ -import { Location } from '@angular/common'; -import { Component } from '@angular/core'; -import { - ActivatedRoute, - Router, -} from '@angular/router'; +import { Location } from "@angular/common"; +import { Component } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; -import { first } from 'rxjs/operators'; +import { first } from "rxjs/operators"; -import { BrowserApi } from '../../browser/browserApi'; +import { BrowserApi } from "../../browser/browserApi"; -import { AuditService } from 'jslib-common/abstractions/audit.service'; -import { CipherService } from 'jslib-common/abstractions/cipher.service'; -import { CollectionService } from 'jslib-common/abstractions/collection.service'; -import { EventService } from 'jslib-common/abstractions/event.service'; -import { FolderService } from 'jslib-common/abstractions/folder.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { OrganizationService } from 'jslib-common/abstractions/organization.service'; -import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService } from 'jslib-common/abstractions/policy.service'; -import { StateService } from '../../services/abstractions/state.service'; +import { AuditService } from "jslib-common/abstractions/audit.service"; +import { CipherService } from "jslib-common/abstractions/cipher.service"; +import { CollectionService } from "jslib-common/abstractions/collection.service"; +import { EventService } from "jslib-common/abstractions/event.service"; +import { FolderService } from "jslib-common/abstractions/folder.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { OrganizationService } from "jslib-common/abstractions/organization.service"; +import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService } from "jslib-common/abstractions/policy.service"; +import { StateService } from "../../services/abstractions/state.service"; -import { PopupUtilsService } from '../services/popup-utils.service'; +import { PopupUtilsService } from "../services/popup-utils.service"; -import { LoginUriView } from 'jslib-common/models/view/loginUriView'; +import { LoginUriView } from "jslib-common/models/view/loginUriView"; -import { AddEditComponent as BaseAddEditComponent } from 'jslib-angular/components/add-edit.component'; +import { AddEditComponent as BaseAddEditComponent } from "jslib-angular/components/add-edit.component"; -import { CipherType } from 'jslib-common/enums/cipherType'; +import { CipherType } from "jslib-common/enums/cipherType"; @Component({ - selector: 'app-vault-add-edit', - templateUrl: 'add-edit.component.html', + selector: "app-vault-add-edit", + templateUrl: "add-edit.component.html", }) export class AddEditComponent extends BaseAddEditComponent { - currentUris: string[]; - showAttachments = true; - openAttachmentsInPopup: boolean; - showAutoFillOnPageLoadOptions: boolean; + currentUris: string[]; + showAttachments = true; + openAttachmentsInPopup: boolean; + showAutoFillOnPageLoadOptions: boolean; - constructor(cipherService: CipherService, folderService: FolderService, - i18nService: I18nService, platformUtilsService: PlatformUtilsService, - auditService: AuditService, stateService: StateService, collectionService: CollectionService, - messagingService: MessagingService, private route: ActivatedRoute, - private router: Router, private location: Location, - eventService: EventService, policyService: PolicyService, - private popupUtilsService: PopupUtilsService, organizationService: OrganizationService, - passwordRepromptService: PasswordRepromptService, logService: LogService) { - super(cipherService, folderService, i18nService, platformUtilsService, - auditService, stateService, collectionService, messagingService, - eventService, policyService, logService, passwordRepromptService, organizationService); - } + constructor( + cipherService: CipherService, + folderService: FolderService, + i18nService: I18nService, + platformUtilsService: PlatformUtilsService, + auditService: AuditService, + stateService: StateService, + collectionService: CollectionService, + messagingService: MessagingService, + private route: ActivatedRoute, + private router: Router, + private location: Location, + eventService: EventService, + policyService: PolicyService, + private popupUtilsService: PopupUtilsService, + organizationService: OrganizationService, + passwordRepromptService: PasswordRepromptService, + logService: LogService + ) { + super( + cipherService, + folderService, + i18nService, + platformUtilsService, + auditService, + stateService, + collectionService, + messagingService, + eventService, + policyService, + logService, + passwordRepromptService, + organizationService + ); + } - async ngOnInit() { - await super.ngOnInit(); + async ngOnInit() { + await super.ngOnInit(); - this.route.queryParams.pipe(first()).subscribe(async params => { - if (params.cipherId) { - this.cipherId = params.cipherId; - } - if (params.folderId) { - this.folderId = params.folderId; - } - if (params.collectionId) { - const collection = this.writeableCollections.find(c => c.id === params.collectionId); - if (collection != null) { - this.collectionIds = [collection.id]; - this.organizationId = collection.organizationId; - } - } - if (params.type) { - const type = parseInt(params.type, null); - this.type = type; - } - this.editMode = !params.cipherId; - - if (params.cloneMode != null) { - this.cloneMode = params.cloneMode === 'true'; - } - await this.load(); - - if (!this.editMode || this.cloneMode) { - if (!this.popupUtilsService.inPopout(window) && params.name && - (this.cipher.name == null || this.cipher.name === '')) { - this.cipher.name = params.name; - } - if (!this.popupUtilsService.inPopout(window) && params.uri && - (this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === '')) { - this.cipher.login.uris[0].uri = params.uri; - } - } - - this.openAttachmentsInPopup = this.popupUtilsService.inPopup(window); - }); - - if (!this.editMode) { - const tabs = await BrowserApi.tabsQuery({ windowType: 'normal' }); - this.currentUris = tabs == null ? null : - tabs.filter(tab => tab.url != null && tab.url !== '').map(tab => tab.url); + this.route.queryParams.pipe(first()).subscribe(async (params) => { + if (params.cipherId) { + this.cipherId = params.cipherId; + } + if (params.folderId) { + this.folderId = params.folderId; + } + if (params.collectionId) { + const collection = this.writeableCollections.find((c) => c.id === params.collectionId); + if (collection != null) { + this.collectionIds = [collection.id]; + this.organizationId = collection.organizationId; } + } + if (params.type) { + const type = parseInt(params.type, null); + this.type = type; + } + this.editMode = !params.cipherId; - window.setTimeout(() => { - if (!this.editMode) { - if (this.cipher.name != null && this.cipher.name !== '') { - document.getElementById('loginUsername').focus(); - } else { - document.getElementById('name').focus(); - } - } - }, 200); - } + if (params.cloneMode != null) { + this.cloneMode = params.cloneMode === "true"; + } + await this.load(); - async load() { - await super.load(); - this.showAutoFillOnPageLoadOptions = this.cipher.type === CipherType.Login && - await this.stateService.getEnableAutoFillOnPageLoad(); - } - - async submit(): Promise { - if (await super.submit()) { - if (this.cloneMode) { - this.router.navigate(['/tabs/vault']); - } else { - this.location.back(); - } - return true; + if (!this.editMode || this.cloneMode) { + if ( + !this.popupUtilsService.inPopout(window) && + params.name && + (this.cipher.name == null || this.cipher.name === "") + ) { + this.cipher.name = params.name; } + if ( + !this.popupUtilsService.inPopout(window) && + params.uri && + (this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === "") + ) { + this.cipher.login.uris[0].uri = params.uri; + } + } - return false; + this.openAttachmentsInPopup = this.popupUtilsService.inPopup(window); + }); + + if (!this.editMode) { + const tabs = await BrowserApi.tabsQuery({ windowType: "normal" }); + this.currentUris = + tabs == null + ? null + : tabs.filter((tab) => tab.url != null && tab.url !== "").map((tab) => tab.url); } - attachments() { - super.attachments(); - - if (this.openAttachmentsInPopup) { - const destinationUrl = this.router.createUrlTree(['/attachments'], { queryParams: { cipherId: this.cipher.id } }).toString(); - const currentBaseUrl = window.location.href.replace(this.router.url, ''); - this.popupUtilsService.popOut(window, currentBaseUrl + destinationUrl); + window.setTimeout(() => { + if (!this.editMode) { + if (this.cipher.name != null && this.cipher.name !== "") { + document.getElementById("loginUsername").focus(); } else { - this.router.navigate(['/attachments'], { queryParams: { cipherId: this.cipher.id } }); + document.getElementById("name").focus(); } - } + } + }, 200); + } + async load() { + await super.load(); + this.showAutoFillOnPageLoadOptions = + this.cipher.type === CipherType.Login && + (await this.stateService.getEnableAutoFillOnPageLoad()); + } - editCollections() { - super.editCollections(); - if (this.cipher.organizationId != null) { - this.router.navigate(['/collections'], { queryParams: { cipherId: this.cipher.id } }); - } - } - - cancel() { - super.cancel(); + async submit(): Promise { + if (await super.submit()) { + if (this.cloneMode) { + this.router.navigate(["/tabs/vault"]); + } else { this.location.back(); + } + return true; } - async generatePassword(): Promise { - const confirmed = await super.generatePassword(); - if (confirmed) { - this.stateService.setAddEditCipherInfo({ - cipher: this.cipher, - collectionIds: this.collections == null ? [] : - this.collections.filter(c => (c as any).checked).map(c => c.id), - }); - this.router.navigate(['generator']); - } - return confirmed; - } + return false; + } - async delete(): Promise { - const confirmed = await super.delete(); - if (confirmed) { - this.router.navigate(['/tabs/vault']); - } - return confirmed; - } + attachments() { + super.attachments(); - toggleUriInput(uri: LoginUriView) { - const u = (uri as any); - u.showCurrentUris = !u.showCurrentUris; + if (this.openAttachmentsInPopup) { + const destinationUrl = this.router + .createUrlTree(["/attachments"], { queryParams: { cipherId: this.cipher.id } }) + .toString(); + const currentBaseUrl = window.location.href.replace(this.router.url, ""); + this.popupUtilsService.popOut(window, currentBaseUrl + destinationUrl); + } else { + this.router.navigate(["/attachments"], { queryParams: { cipherId: this.cipher.id } }); } + } - allowOwnershipOptions(): boolean { - return (!this.editMode || this.cloneMode) && this.ownershipOptions - && (this.ownershipOptions.length > 1 || !this.allowPersonal); + editCollections() { + super.editCollections(); + if (this.cipher.organizationId != null) { + this.router.navigate(["/collections"], { queryParams: { cipherId: this.cipher.id } }); } + } + + cancel() { + super.cancel(); + this.location.back(); + } + + async generatePassword(): Promise { + const confirmed = await super.generatePassword(); + if (confirmed) { + this.stateService.setAddEditCipherInfo({ + cipher: this.cipher, + collectionIds: + this.collections == null + ? [] + : this.collections.filter((c) => (c as any).checked).map((c) => c.id), + }); + this.router.navigate(["generator"]); + } + return confirmed; + } + + async delete(): Promise { + const confirmed = await super.delete(); + if (confirmed) { + this.router.navigate(["/tabs/vault"]); + } + return confirmed; + } + + toggleUriInput(uri: LoginUriView) { + const u = uri as any; + u.showCurrentUris = !u.showCurrentUris; + } + + allowOwnershipOptions(): boolean { + return ( + (!this.editMode || this.cloneMode) && + this.ownershipOptions && + (this.ownershipOptions.length > 1 || !this.allowPersonal) + ); + } } diff --git a/src/popup/vault/attachments.component.ts b/src/popup/vault/attachments.component.ts index 0cb9f0c02d..346ad65074 100644 --- a/src/popup/vault/attachments.component.ts +++ b/src/popup/vault/attachments.component.ts @@ -1,48 +1,63 @@ -import { Location } from '@angular/common'; -import { Component } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { Location } from "@angular/common"; +import { Component } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; -import { first } from 'rxjs/operators'; +import { first } from "rxjs/operators"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { CipherService } from 'jslib-common/abstractions/cipher.service'; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { CipherService } from "jslib-common/abstractions/cipher.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; -import { AttachmentsComponent as BaseAttachmentsComponent } from 'jslib-angular/components/attachments.component'; -import { StateService } from '../../services/abstractions/state.service'; +import { AttachmentsComponent as BaseAttachmentsComponent } from "jslib-angular/components/attachments.component"; +import { StateService } from "../../services/abstractions/state.service"; @Component({ - selector: 'app-vault-attachments', - templateUrl: 'attachments.component.html', + selector: "app-vault-attachments", + templateUrl: "attachments.component.html", }) export class AttachmentsComponent extends BaseAttachmentsComponent { - openedAttachmentsInPopup: boolean; + openedAttachmentsInPopup: boolean; - constructor(cipherService: CipherService, i18nService: I18nService, - cryptoService: CryptoService, platformUtilsService: PlatformUtilsService, - apiService: ApiService, private location: Location, - private route: ActivatedRoute, stateService: StateService, logService: LogService) { - super(cipherService, i18nService, cryptoService, platformUtilsService, - apiService, window, logService, stateService); - } + constructor( + cipherService: CipherService, + i18nService: I18nService, + cryptoService: CryptoService, + platformUtilsService: PlatformUtilsService, + apiService: ApiService, + private location: Location, + private route: ActivatedRoute, + stateService: StateService, + logService: LogService + ) { + super( + cipherService, + i18nService, + cryptoService, + platformUtilsService, + apiService, + window, + logService, + stateService + ); + } - async ngOnInit() { - this.route.queryParams.pipe(first()).subscribe(async params => { - this.cipherId = params.cipherId; - await this.init(); - }); + async ngOnInit() { + this.route.queryParams.pipe(first()).subscribe(async (params) => { + this.cipherId = params.cipherId; + await this.init(); + }); - this.openedAttachmentsInPopup = history.length === 1; - } + this.openedAttachmentsInPopup = history.length === 1; + } - back() { - this.location.back(); - } + back() { + this.location.back(); + } - close() { - window.close(); - } + close() { + window.close(); + } } diff --git a/src/popup/vault/ciphers.component.ts b/src/popup/vault/ciphers.component.ts index 42f6adea83..80fd9d045a 100644 --- a/src/popup/vault/ciphers.component.ts +++ b/src/popup/vault/ciphers.component.ts @@ -1,235 +1,247 @@ -import { Location } from '@angular/common'; -import { - ChangeDetectorRef, - Component, - NgZone, - OnDestroy, - OnInit, -} from '@angular/core'; -import { - ActivatedRoute, - Router, -} from '@angular/router'; +import { Location } from "@angular/common"; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; -import { first } from 'rxjs/operators'; +import { first } from "rxjs/operators"; -import { BrowserApi } from '../../browser/browserApi'; +import { BrowserApi } from "../../browser/browserApi"; -import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; -import { CipherService } from 'jslib-common/abstractions/cipher.service'; -import { CollectionService } from 'jslib-common/abstractions/collection.service'; -import { FolderService } from 'jslib-common/abstractions/folder.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { SearchService } from 'jslib-common/abstractions/search.service'; -import { StateService } from '../../services/abstractions/state.service'; +import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; +import { CipherService } from "jslib-common/abstractions/cipher.service"; +import { CollectionService } from "jslib-common/abstractions/collection.service"; +import { FolderService } from "jslib-common/abstractions/folder.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { SearchService } from "jslib-common/abstractions/search.service"; +import { StateService } from "../../services/abstractions/state.service"; -import { CipherType } from 'jslib-common/enums/cipherType'; +import { CipherType } from "jslib-common/enums/cipherType"; -import { CipherView } from 'jslib-common/models/view/cipherView'; -import { CollectionView } from 'jslib-common/models/view/collectionView'; -import { FolderView } from 'jslib-common/models/view/folderView'; +import { CipherView } from "jslib-common/models/view/cipherView"; +import { CollectionView } from "jslib-common/models/view/collectionView"; +import { FolderView } from "jslib-common/models/view/folderView"; -import { TreeNode } from 'jslib-common/models/domain/treeNode'; +import { TreeNode } from "jslib-common/models/domain/treeNode"; -import { CiphersComponent as BaseCiphersComponent } from 'jslib-angular/components/ciphers.component'; +import { CiphersComponent as BaseCiphersComponent } from "jslib-angular/components/ciphers.component"; -import { BrowserComponentState } from '../../models/browserComponentState'; -import { PopupUtilsService } from '../services/popup-utils.service'; +import { BrowserComponentState } from "../../models/browserComponentState"; +import { PopupUtilsService } from "../services/popup-utils.service"; -const ComponentId = 'CiphersComponent'; +const ComponentId = "CiphersComponent"; @Component({ - selector: 'app-vault-ciphers', - templateUrl: 'ciphers.component.html', + selector: "app-vault-ciphers", + templateUrl: "ciphers.component.html", }) export class CiphersComponent extends BaseCiphersComponent implements OnInit, OnDestroy { - groupingTitle: string; - state: BrowserComponentState; - folderId: string = null; - collectionId: string = null; - type: CipherType = null; - nestedFolders: TreeNode[]; - nestedCollections: TreeNode[]; - searchTypeSearch = false; + groupingTitle: string; + state: BrowserComponentState; + folderId: string = null; + collectionId: string = null; + type: CipherType = null; + nestedFolders: TreeNode[]; + nestedCollections: TreeNode[]; + searchTypeSearch = false; - private selectedTimeout: number; - private preventSelected = false; - private applySavedState = true; - private scrollingContainer = 'cdk-virtual-scroll-viewport'; + private selectedTimeout: number; + private preventSelected = false; + private applySavedState = true; + private scrollingContainer = "cdk-virtual-scroll-viewport"; - constructor(searchService: SearchService, private route: ActivatedRoute, - private router: Router, private location: Location, - private ngZone: NgZone, private broadcasterService: BroadcasterService, - private changeDetectorRef: ChangeDetectorRef, private stateService: StateService, - private popupUtils: PopupUtilsService, private i18nService: I18nService, - private folderService: FolderService, private collectionService: CollectionService, - private platformUtilsService: PlatformUtilsService, private cipherService: CipherService) { - super(searchService); - this.applySavedState = (window as any).previousPopupUrl != null && - !(window as any).previousPopupUrl.startsWith('/ciphers'); - } + constructor( + searchService: SearchService, + private route: ActivatedRoute, + private router: Router, + private location: Location, + private ngZone: NgZone, + private broadcasterService: BroadcasterService, + private changeDetectorRef: ChangeDetectorRef, + private stateService: StateService, + private popupUtils: PopupUtilsService, + private i18nService: I18nService, + private folderService: FolderService, + private collectionService: CollectionService, + private platformUtilsService: PlatformUtilsService, + private cipherService: CipherService + ) { + super(searchService); + this.applySavedState = + (window as any).previousPopupUrl != null && + !(window as any).previousPopupUrl.startsWith("/ciphers"); + } - async ngOnInit() { - this.searchTypeSearch = !this.platformUtilsService.isSafari(); - this.route.queryParams.pipe(first()).subscribe(async params => { - if (this.applySavedState) { - this.state = await this.stateService.getBrowserCipherComponentState(); - if (this.state?.searchText) { - this.searchText = this.state.searchText; - } + async ngOnInit() { + this.searchTypeSearch = !this.platformUtilsService.isSafari(); + this.route.queryParams.pipe(first()).subscribe(async (params) => { + if (this.applySavedState) { + this.state = await this.stateService.getBrowserCipherComponentState(); + if (this.state?.searchText) { + this.searchText = this.state.searchText; + } + } + + if (params.deleted) { + this.groupingTitle = this.i18nService.t("trash"); + this.searchPlaceholder = this.i18nService.t("searchTrash"); + await this.load(null, true); + } else if (params.type) { + this.searchPlaceholder = this.i18nService.t("searchType"); + this.type = parseInt(params.type, null); + switch (this.type) { + case CipherType.Login: + this.groupingTitle = this.i18nService.t("logins"); + break; + case CipherType.Card: + this.groupingTitle = this.i18nService.t("cards"); + break; + case CipherType.Identity: + this.groupingTitle = this.i18nService.t("identities"); + break; + case CipherType.SecureNote: + this.groupingTitle = this.i18nService.t("secureNotes"); + break; + default: + break; + } + await this.load((c) => c.type === this.type); + } else if (params.folderId) { + this.folderId = params.folderId === "none" ? null : params.folderId; + this.searchPlaceholder = this.i18nService.t("searchFolder"); + if (this.folderId != null) { + const folderNode = await this.folderService.getNested(this.folderId); + if (folderNode != null && folderNode.node != null) { + this.groupingTitle = folderNode.node.name; + this.nestedFolders = + folderNode.children != null && folderNode.children.length > 0 + ? folderNode.children + : null; + } + } else { + this.groupingTitle = this.i18nService.t("noneFolder"); + } + await this.load((c) => c.folderId === this.folderId); + } else if (params.collectionId) { + this.collectionId = params.collectionId; + this.searchPlaceholder = this.i18nService.t("searchCollection"); + const collectionNode = await this.collectionService.getNested(this.collectionId); + if (collectionNode != null && collectionNode.node != null) { + this.groupingTitle = collectionNode.node.name; + this.nestedCollections = + collectionNode.children != null && collectionNode.children.length > 0 + ? collectionNode.children + : null; + } + await this.load( + (c) => c.collectionIds != null && c.collectionIds.indexOf(this.collectionId) > -1 + ); + } else { + this.groupingTitle = this.i18nService.t("allItems"); + await this.load(); + } + + if (this.applySavedState && this.state != null) { + window.setTimeout( + () => + this.popupUtils.setContentScrollY(window, this.state?.scrollY, this.scrollingContainer), + 0 + ); + } + await this.stateService.setBrowserCipherComponentState(null); + }); + + this.broadcasterService.subscribe(ComponentId, (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case "syncCompleted": + if (message.successfully) { + window.setTimeout(() => { + this.refresh(); + }, 500); } - - if (params.deleted) { - this.groupingTitle = this.i18nService.t('trash'); - this.searchPlaceholder = this.i18nService.t('searchTrash'); - await this.load(null, true); - } else if (params.type) { - this.searchPlaceholder = this.i18nService.t('searchType'); - this.type = parseInt(params.type, null); - switch (this.type) { - case CipherType.Login: - this.groupingTitle = this.i18nService.t('logins'); - break; - case CipherType.Card: - this.groupingTitle = this.i18nService.t('cards'); - break; - case CipherType.Identity: - this.groupingTitle = this.i18nService.t('identities'); - break; - case CipherType.SecureNote: - this.groupingTitle = this.i18nService.t('secureNotes'); - break; - default: - break; - } - await this.load(c => c.type === this.type); - } else if (params.folderId) { - this.folderId = params.folderId === 'none' ? null : params.folderId; - this.searchPlaceholder = this.i18nService.t('searchFolder'); - if (this.folderId != null) { - const folderNode = await this.folderService.getNested(this.folderId); - if (folderNode != null && folderNode.node != null) { - this.groupingTitle = folderNode.node.name; - this.nestedFolders = folderNode.children != null && folderNode.children.length > 0 ? - folderNode.children : null; - } - } else { - this.groupingTitle = this.i18nService.t('noneFolder'); - } - await this.load(c => c.folderId === this.folderId); - } else if (params.collectionId) { - this.collectionId = params.collectionId; - this.searchPlaceholder = this.i18nService.t('searchCollection'); - const collectionNode = await this.collectionService.getNested(this.collectionId); - if (collectionNode != null && collectionNode.node != null) { - this.groupingTitle = collectionNode.node.name; - this.nestedCollections = collectionNode.children != null && collectionNode.children.length > 0 ? - collectionNode.children : null; - } - await this.load(c => c.collectionIds != null && c.collectionIds.indexOf(this.collectionId) > -1); - } else { - this.groupingTitle = this.i18nService.t('allItems'); - await this.load(); - } - - if (this.applySavedState && this.state != null) { - window.setTimeout(() => this.popupUtils.setContentScrollY(window, this.state?.scrollY, - this.scrollingContainer), 0); - } - await this.stateService.setBrowserCipherComponentState(null); - }); - - this.broadcasterService.subscribe(ComponentId, (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case 'syncCompleted': - if (message.successfully) { - window.setTimeout(() => { - this.refresh(); - }, 500); - } - break; - default: - break; - } - - this.changeDetectorRef.detectChanges(); - }); - }); - } - - ngOnDestroy() { - this.saveState(); - this.broadcasterService.unsubscribe(ComponentId); - } - - selectCipher(cipher: CipherView) { - this.selectedTimeout = window.setTimeout(() => { - if (!this.preventSelected) { - super.selectCipher(cipher); - this.router.navigate(['/view-cipher'], { queryParams: { cipherId: cipher.id } }); - } - this.preventSelected = false; - }, 200); - } - - selectFolder(folder: FolderView) { - if (folder.id != null) { - this.router.navigate(['/ciphers'], { queryParams: { folderId: folder.id } }); - } - } - - selectCollection(collection: CollectionView) { - this.router.navigate(['/ciphers'], { queryParams: { collectionId: collection.id } }); - } - - async launchCipher(cipher: CipherView) { - if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) { - return; + break; + default: + break; } - if (this.selectedTimeout != null) { - window.clearTimeout(this.selectedTimeout); - } - this.preventSelected = true; - await this.cipherService.updateLastLaunchedDate(cipher.id); - BrowserApi.createNewTab(cipher.login.launchUri); - if (this.popupUtils.inPopup(window)) { - BrowserApi.closePopup(window); - } + this.changeDetectorRef.detectChanges(); + }); + }); + } + + ngOnDestroy() { + this.saveState(); + this.broadcasterService.unsubscribe(ComponentId); + } + + selectCipher(cipher: CipherView) { + this.selectedTimeout = window.setTimeout(() => { + if (!this.preventSelected) { + super.selectCipher(cipher); + this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } }); + } + this.preventSelected = false; + }, 200); + } + + selectFolder(folder: FolderView) { + if (folder.id != null) { + this.router.navigate(["/ciphers"], { queryParams: { folderId: folder.id } }); + } + } + + selectCollection(collection: CollectionView) { + this.router.navigate(["/ciphers"], { queryParams: { collectionId: collection.id } }); + } + + async launchCipher(cipher: CipherView) { + if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) { + return; } - addCipher() { - if (this.deleted) { - return false; - } - super.addCipher(); - this.router.navigate(['/add-cipher'], { - queryParams: { - folderId: this.folderId, - type: this.type, - collectionId: this.collectionId, - }, - }); + if (this.selectedTimeout != null) { + window.clearTimeout(this.selectedTimeout); } + this.preventSelected = true; + await this.cipherService.updateLastLaunchedDate(cipher.id); + BrowserApi.createNewTab(cipher.login.launchUri); + if (this.popupUtils.inPopup(window)) { + BrowserApi.closePopup(window); + } + } - back() { - (window as any).routeDirection = 'b'; - this.location.back(); + addCipher() { + if (this.deleted) { + return false; } + super.addCipher(); + this.router.navigate(["/add-cipher"], { + queryParams: { + folderId: this.folderId, + type: this.type, + collectionId: this.collectionId, + }, + }); + } - showGroupings() { - return !this.isSearching() && - ((this.nestedFolders && this.nestedFolders.length) || - (this.nestedCollections && this.nestedCollections.length)); - } + back() { + (window as any).routeDirection = "b"; + this.location.back(); + } - private async saveState() { - this.state = { - scrollY: this.popupUtils.getContentScrollY(window, this.scrollingContainer), - searchText: this.searchText, - }; - await this.stateService.setBrowserCipherComponentState(this.state); - } + showGroupings() { + return ( + !this.isSearching() && + ((this.nestedFolders && this.nestedFolders.length) || + (this.nestedCollections && this.nestedCollections.length)) + ); + } + + private async saveState() { + this.state = { + scrollY: this.popupUtils.getContentScrollY(window, this.scrollingContainer), + searchText: this.searchText, + }; + await this.stateService.setBrowserCipherComponentState(this.state); + } } diff --git a/src/popup/vault/collections.component.ts b/src/popup/vault/collections.component.ts index 7cbfc30a4c..d31e492bef 100644 --- a/src/popup/vault/collections.component.ts +++ b/src/popup/vault/collections.component.ts @@ -1,38 +1,44 @@ -import { Location } from '@angular/common'; -import { Component } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { first } from 'rxjs/operators'; +import { Location } from "@angular/common"; +import { Component } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { first } from "rxjs/operators"; -import { CipherService } from 'jslib-common/abstractions/cipher.service'; -import { CollectionService } from 'jslib-common/abstractions/collection.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { CipherService } from "jslib-common/abstractions/cipher.service"; +import { CollectionService } from "jslib-common/abstractions/collection.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; -import { CollectionsComponent as BaseCollectionsComponent } from 'jslib-angular/components/collections.component'; +import { CollectionsComponent as BaseCollectionsComponent } from "jslib-angular/components/collections.component"; @Component({ - selector: 'app-vault-collections', - templateUrl: 'collections.component.html', + selector: "app-vault-collections", + templateUrl: "collections.component.html", }) export class CollectionsComponent extends BaseCollectionsComponent { - constructor(collectionService: CollectionService, platformUtilsService: PlatformUtilsService, - i18nService: I18nService, cipherService: CipherService, - private route: ActivatedRoute, private location: Location, logService: LogService) { - super(collectionService, platformUtilsService, i18nService, cipherService, logService); - } + constructor( + collectionService: CollectionService, + platformUtilsService: PlatformUtilsService, + i18nService: I18nService, + cipherService: CipherService, + private route: ActivatedRoute, + private location: Location, + logService: LogService + ) { + super(collectionService, platformUtilsService, i18nService, cipherService, logService); + } - async ngOnInit() { - this.onSavedCollections.subscribe(() => { - this.back(); - }); - this.route.queryParams.pipe(first()).subscribe(async params => { - this.cipherId = params.cipherId; - await this.load(); - }); - } + async ngOnInit() { + this.onSavedCollections.subscribe(() => { + this.back(); + }); + this.route.queryParams.pipe(first()).subscribe(async (params) => { + this.cipherId = params.cipherId; + await this.load(); + }); + } - back() { - this.location.back(); - } + back() { + this.location.back(); + } } diff --git a/src/popup/vault/current-tab.component.ts b/src/popup/vault/current-tab.component.ts index b412a2862a..60f57b7b64 100644 --- a/src/popup/vault/current-tab.component.ts +++ b/src/popup/vault/current-tab.component.ts @@ -1,241 +1,249 @@ -import { - ChangeDetectorRef, - Component, - NgZone, - OnDestroy, - OnInit, -} from '@angular/core'; -import { Router } from '@angular/router'; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; -import { BrowserApi } from '../../browser/browserApi'; +import { BrowserApi } from "../../browser/browserApi"; -import { CipherRepromptType } from 'jslib-common/enums/cipherRepromptType'; -import { CipherType } from 'jslib-common/enums/cipherType'; +import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType"; +import { CipherType } from "jslib-common/enums/cipherType"; -import { CipherView } from 'jslib-common/models/view/cipherView'; +import { CipherView } from "jslib-common/models/view/cipherView"; -import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; -import { CipherService } from 'jslib-common/abstractions/cipher.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { SearchService } from 'jslib-common/abstractions/search.service'; -import { SyncService } from 'jslib-common/abstractions/sync.service'; +import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; +import { CipherService } from "jslib-common/abstractions/cipher.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { SearchService } from "jslib-common/abstractions/search.service"; +import { SyncService } from "jslib-common/abstractions/sync.service"; -import { AutofillService } from '../../services/abstractions/autofill.service'; -import { StateService } from '../../services/abstractions/state.service'; +import { AutofillService } from "../../services/abstractions/autofill.service"; +import { StateService } from "../../services/abstractions/state.service"; -import { PopupUtilsService } from '../services/popup-utils.service'; +import { PopupUtilsService } from "../services/popup-utils.service"; -import { Utils } from 'jslib-common/misc/utils'; +import { Utils } from "jslib-common/misc/utils"; -const BroadcasterSubscriptionId = 'CurrentTabComponent'; +const BroadcasterSubscriptionId = "CurrentTabComponent"; @Component({ - selector: 'app-current-tab', - templateUrl: 'current-tab.component.html', + selector: "app-current-tab", + templateUrl: "current-tab.component.html", }) export class CurrentTabComponent implements OnInit, OnDestroy { - pageDetails: any[] = []; - cardCiphers: CipherView[]; - identityCiphers: CipherView[]; - loginCiphers: CipherView[]; - url: string; - hostname: string; - searchText: string; - inSidebar = false; - searchTypeSearch = false; - loaded = false; + pageDetails: any[] = []; + cardCiphers: CipherView[]; + identityCiphers: CipherView[]; + loginCiphers: CipherView[]; + url: string; + hostname: string; + searchText: string; + inSidebar = false; + searchTypeSearch = false; + loaded = false; - private totpCode: string; - private totpTimeout: number; - private loadedTimeout: number; - private searchTimeout: number; + private totpCode: string; + private totpTimeout: number; + private loadedTimeout: number; + private searchTimeout: number; - constructor(private platformUtilsService: PlatformUtilsService, private cipherService: CipherService, - private popupUtilsService: PopupUtilsService, private autofillService: AutofillService, - private i18nService: I18nService, private router: Router, - private ngZone: NgZone, private broadcasterService: BroadcasterService, - private changeDetectorRef: ChangeDetectorRef, private syncService: SyncService, - private searchService: SearchService, private stateService: StateService, - private passwordRepromptService: PasswordRepromptService) { + constructor( + private platformUtilsService: PlatformUtilsService, + private cipherService: CipherService, + private popupUtilsService: PopupUtilsService, + private autofillService: AutofillService, + private i18nService: I18nService, + private router: Router, + private ngZone: NgZone, + private broadcasterService: BroadcasterService, + private changeDetectorRef: ChangeDetectorRef, + private syncService: SyncService, + private searchService: SearchService, + private stateService: StateService, + private passwordRepromptService: PasswordRepromptService + ) {} + + async ngOnInit() { + this.searchTypeSearch = !this.platformUtilsService.isSafari(); + this.inSidebar = this.popupUtilsService.inSidebar(window); + + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case "syncCompleted": + if (this.loaded) { + window.setTimeout(() => { + this.load(); + }, 500); + } + break; + case "collectPageDetailsResponse": + if (message.sender === BroadcasterSubscriptionId) { + this.pageDetails.push({ + frameId: message.webExtSender.frameId, + tab: message.tab, + details: message.details, + }); + } + break; + default: + break; + } + + this.changeDetectorRef.detectChanges(); + }); + }); + + if (!this.syncService.syncInProgress) { + await this.load(); + } else { + this.loadedTimeout = window.setTimeout(async () => { + if (!this.loaded) { + await this.load(); + } + }, 5000); } - async ngOnInit() { - this.searchTypeSearch = !this.platformUtilsService.isSafari(); - this.inSidebar = this.popupUtilsService.inSidebar(window); + window.setTimeout(() => { + document.getElementById("search").focus(); + }, 100); + } - this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case 'syncCompleted': - if (this.loaded) { - window.setTimeout(() => { - this.load(); - }, 500); - } - break; - case 'collectPageDetailsResponse': - if (message.sender === BroadcasterSubscriptionId) { - this.pageDetails.push({ - frameId: message.webExtSender.frameId, - tab: message.tab, - details: message.details, - }); - } - break; - default: - break; - } + ngOnDestroy() { + window.clearTimeout(this.loadedTimeout); + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + } - this.changeDetectorRef.detectChanges(); - }); - }); + async refresh() { + await this.load(); + } - if (!this.syncService.syncInProgress) { - await this.load(); + addCipher() { + this.router.navigate(["/add-cipher"], { queryParams: { name: this.hostname, uri: this.url } }); + } + + viewCipher(cipher: CipherView) { + this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } }); + } + + async fillCipher(cipher: CipherView) { + if ( + cipher.reprompt !== CipherRepromptType.None && + !(await this.passwordRepromptService.showPasswordPrompt()) + ) { + return; + } + + this.totpCode = null; + if (this.totpTimeout != null) { + window.clearTimeout(this.totpTimeout); + } + + if (this.pageDetails == null || this.pageDetails.length === 0) { + this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError")); + return; + } + + try { + this.totpCode = await this.autofillService.doAutoFill({ + cipher: cipher, + pageDetails: this.pageDetails, + doc: window.document, + fillNewPassword: true, + }); + if (this.totpCode != null) { + this.platformUtilsService.copyToClipboard(this.totpCode, { window: window }); + } + if (this.popupUtilsService.inPopup(window)) { + if (this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()) { + BrowserApi.closePopup(window); } else { - this.loadedTimeout = window.setTimeout(async () => { - if (!this.loaded) { - await this.load(); - } - }, 5000); + // Slight delay to fix bug in Chromium browsers where popup closes without copying totp to clipboard + setTimeout(() => BrowserApi.closePopup(window), 50); } + } + } catch { + this.ngZone.run(() => { + this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError")); + this.changeDetectorRef.detectChanges(); + }); + } + } - window.setTimeout(() => { - document.getElementById('search').focus(); - }, 100); + searchVault() { + if (this.searchTimeout != null) { + clearTimeout(this.searchTimeout); + } + if (!this.searchService.isSearchable(this.searchText)) { + return; + } + this.searchTimeout = window.setTimeout(async () => { + this.router.navigate(["/tabs/vault"], { queryParams: { searchText: this.searchText } }); + }, 200); + } + + closeOnEsc(e: KeyboardEvent) { + // If input not empty, use browser default behavior of clearing input instead + if (e.key === "Escape" && (this.searchText == null || this.searchText === "")) { + BrowserApi.closePopup(window); + } + } + + private async load() { + const tab = await BrowserApi.getTabFromCurrentWindow(); + if (tab != null) { + this.url = tab.url; + } else { + this.loginCiphers = []; + this.loaded = true; + return; } - ngOnDestroy() { - window.clearTimeout(this.loadedTimeout); - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + this.hostname = Utils.getHostname(this.url); + this.pageDetails = []; + BrowserApi.tabSendMessage(tab, { + command: "collectPageDetails", + tab: tab, + sender: BroadcasterSubscriptionId, + }); + + const otherTypes: CipherType[] = []; + const dontShowCards = await this.stateService.getDontShowCardsCurrentTab(); + const dontShowIdentities = await this.stateService.getDontShowIdentitiesCurrentTab(); + if (!dontShowCards) { + otherTypes.push(CipherType.Card); + } + if (!dontShowIdentities) { + otherTypes.push(CipherType.Identity); } - async refresh() { - await this.load(); - } + const ciphers = await this.cipherService.getAllDecryptedForUrl( + this.url, + otherTypes.length > 0 ? otherTypes : null + ); - addCipher() { - this.router.navigate(['/add-cipher'], { queryParams: { name: this.hostname, uri: this.url } }); - } + this.loginCiphers = []; + this.cardCiphers = []; + this.identityCiphers = []; - viewCipher(cipher: CipherView) { - this.router.navigate(['/view-cipher'], { queryParams: { cipherId: cipher.id } }); - } + ciphers.forEach((c) => { + switch (c.type) { + case CipherType.Login: + this.loginCiphers.push(c); + break; + case CipherType.Card: + this.cardCiphers.push(c); + break; + case CipherType.Identity: + this.identityCiphers.push(c); + break; + default: + break; + } + }); - async fillCipher(cipher: CipherView) { - if (cipher.reprompt !== CipherRepromptType.None && !await this.passwordRepromptService.showPasswordPrompt()) { - return; - } - - this.totpCode = null; - if (this.totpTimeout != null) { - window.clearTimeout(this.totpTimeout); - } - - if (this.pageDetails == null || this.pageDetails.length === 0) { - this.platformUtilsService.showToast('error', null, this.i18nService.t('autofillError')); - return; - } - - try { - this.totpCode = await this.autofillService.doAutoFill({ - cipher: cipher, - pageDetails: this.pageDetails, - doc: window.document, - fillNewPassword: true, - }); - if (this.totpCode != null) { - this.platformUtilsService.copyToClipboard(this.totpCode, { window: window }); - } - if (this.popupUtilsService.inPopup(window)) { - if (this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()) { - BrowserApi.closePopup(window); - } else { - // Slight delay to fix bug in Chromium browsers where popup closes without copying totp to clipboard - setTimeout(() => BrowserApi.closePopup(window), 50); - } - } - } catch { - this.ngZone.run(() => { - this.platformUtilsService.showToast('error', null, this.i18nService.t('autofillError')); - this.changeDetectorRef.detectChanges(); - }); - } - } - - searchVault() { - if (this.searchTimeout != null) { - clearTimeout(this.searchTimeout); - } - if (!this.searchService.isSearchable(this.searchText)) { - return; - } - this.searchTimeout = window.setTimeout(async () => { - this.router.navigate(['/tabs/vault'], { queryParams: { searchText: this.searchText } }); - }, 200); - } - - closeOnEsc(e: KeyboardEvent) { - // If input not empty, use browser default behavior of clearing input instead - if (e.key === 'Escape' && (this.searchText == null || this.searchText === '')) { - BrowserApi.closePopup(window); - } - } - - private async load() { - const tab = await BrowserApi.getTabFromCurrentWindow(); - if (tab != null) { - this.url = tab.url; - } else { - this.loginCiphers = []; - this.loaded = true; - return; - } - - this.hostname = Utils.getHostname(this.url); - this.pageDetails = []; - BrowserApi.tabSendMessage(tab, { - command: 'collectPageDetails', - tab: tab, - sender: BroadcasterSubscriptionId, - }); - - const otherTypes: CipherType[] = []; - const dontShowCards = await this.stateService.getDontShowCardsCurrentTab(); - const dontShowIdentities = await this.stateService.getDontShowIdentitiesCurrentTab(); - if (!dontShowCards) { - otherTypes.push(CipherType.Card); - } - if (!dontShowIdentities) { - otherTypes.push(CipherType.Identity); - } - - const ciphers = await this.cipherService.getAllDecryptedForUrl(this.url, - otherTypes.length > 0 ? otherTypes : null); - - this.loginCiphers = []; - this.cardCiphers = []; - this.identityCiphers = []; - - ciphers.forEach(c => { - switch (c.type) { - case CipherType.Login: - this.loginCiphers.push(c); - break; - case CipherType.Card: - this.cardCiphers.push(c); - break; - case CipherType.Identity: - this.identityCiphers.push(c); - break; - default: - break; - } - }); - - this.loginCiphers = this.loginCiphers.sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b)); - this.loaded = true; - } + this.loginCiphers = this.loginCiphers.sort((a, b) => + this.cipherService.sortCiphersByLastUsedThenName(a, b) + ); + this.loaded = true; + } } diff --git a/src/popup/vault/groupings.component.ts b/src/popup/vault/groupings.component.ts index bde3a484b1..5ffcb905ac 100644 --- a/src/popup/vault/groupings.component.ts +++ b/src/popup/vault/groupings.component.ts @@ -1,355 +1,369 @@ -import { Location } from '@angular/common'; -import { - ChangeDetectorRef, - Component, - NgZone, - OnDestroy, - OnInit, -} from '@angular/core'; -import { - ActivatedRoute, - Router, -} from '@angular/router'; +import { Location } from "@angular/common"; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; -import { first } from 'rxjs/operators'; +import { first } from "rxjs/operators"; -import { BrowserApi } from '../../browser/browserApi'; +import { BrowserApi } from "../../browser/browserApi"; -import { CipherType } from 'jslib-common/enums/cipherType'; +import { CipherType } from "jslib-common/enums/cipherType"; -import { CipherView } from 'jslib-common/models/view/cipherView'; -import { CollectionView } from 'jslib-common/models/view/collectionView'; -import { FolderView } from 'jslib-common/models/view/folderView'; +import { CipherView } from "jslib-common/models/view/cipherView"; +import { CollectionView } from "jslib-common/models/view/collectionView"; +import { FolderView } from "jslib-common/models/view/folderView"; -import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; -import { CipherService } from 'jslib-common/abstractions/cipher.service'; -import { CollectionService } from 'jslib-common/abstractions/collection.service'; -import { FolderService } from 'jslib-common/abstractions/folder.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { SearchService } from 'jslib-common/abstractions/search.service'; -import { SyncService } from 'jslib-common/abstractions/sync.service'; +import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; +import { CipherService } from "jslib-common/abstractions/cipher.service"; +import { CollectionService } from "jslib-common/abstractions/collection.service"; +import { FolderService } from "jslib-common/abstractions/folder.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { SearchService } from "jslib-common/abstractions/search.service"; +import { SyncService } from "jslib-common/abstractions/sync.service"; -import { GroupingsComponent as BaseGroupingsComponent } from 'jslib-angular/components/groupings.component'; +import { GroupingsComponent as BaseGroupingsComponent } from "jslib-angular/components/groupings.component"; -import { StateService } from '../../services/abstractions/state.service'; -import { PopupUtilsService } from '../services/popup-utils.service'; +import { StateService } from "../../services/abstractions/state.service"; +import { PopupUtilsService } from "../services/popup-utils.service"; -const ComponentId = 'GroupingsComponent'; -const ScopeStateId = ComponentId + 'Scope'; +const ComponentId = "GroupingsComponent"; +const ScopeStateId = ComponentId + "Scope"; @Component({ - selector: 'app-vault-groupings', - templateUrl: 'groupings.component.html', + selector: "app-vault-groupings", + templateUrl: "groupings.component.html", }) export class GroupingsComponent extends BaseGroupingsComponent implements OnInit, OnDestroy { + get showNoFolderCiphers(): boolean { + return ( + this.noFolderCiphers != null && + this.noFolderCiphers.length < this.noFolderListSize && + this.collections.length === 0 + ); + } - get showNoFolderCiphers(): boolean { - return this.noFolderCiphers != null && this.noFolderCiphers.length < this.noFolderListSize && - this.collections.length === 0; - } + get folderCount(): number { + return this.nestedFolders.length - (this.showNoFolderCiphers ? 0 : 1); + } + ciphers: CipherView[]; + favoriteCiphers: CipherView[]; + noFolderCiphers: CipherView[]; + folderCounts = new Map(); + collectionCounts = new Map(); + typeCounts = new Map(); + searchText: string; + state: any; + scopeState: any; + showLeftHeader = true; + searchPending = false; + searchTypeSearch = false; + deletedCount = 0; - get folderCount(): number { - return this.nestedFolders.length - (this.showNoFolderCiphers ? 0 : 1); - } - ciphers: CipherView[]; - favoriteCiphers: CipherView[]; - noFolderCiphers: CipherView[]; - folderCounts = new Map(); - collectionCounts = new Map(); - typeCounts = new Map(); - searchText: string; - state: any; - scopeState: any; - showLeftHeader = true; - searchPending = false; - searchTypeSearch = false; - deletedCount = 0; + private loadedTimeout: number; + private selectedTimeout: number; + private preventSelected = false; + private noFolderListSize = 100; + private searchTimeout: any = null; + private hasSearched = false; + private hasLoadedAllCiphers = false; + private allCiphers: CipherView[] = null; - private loadedTimeout: number; - private selectedTimeout: number; - private preventSelected = false; - private noFolderListSize = 100; - private searchTimeout: any = null; - private hasSearched = false; - private hasLoadedAllCiphers = false; - private allCiphers: CipherView[] = null; + constructor( + collectionService: CollectionService, + folderService: FolderService, + private cipherService: CipherService, + private router: Router, + private ngZone: NgZone, + private broadcasterService: BroadcasterService, + private changeDetectorRef: ChangeDetectorRef, + private route: ActivatedRoute, + baseStateService: StateService, + private popupUtils: PopupUtilsService, + private syncService: SyncService, + private platformUtilsService: PlatformUtilsService, + private searchService: SearchService, + private location: Location, + private browserStateService: StateService + ) { + super(collectionService, folderService, baseStateService); + this.noFolderListSize = 100; + } - constructor(collectionService: CollectionService, folderService: FolderService, - private cipherService: CipherService, private router: Router, - private ngZone: NgZone, private broadcasterService: BroadcasterService, - private changeDetectorRef: ChangeDetectorRef, private route: ActivatedRoute, - baseStateService: StateService, private popupUtils: PopupUtilsService, - private syncService: SyncService, private platformUtilsService: PlatformUtilsService, - private searchService: SearchService, private location: Location, - private browserStateService: StateService) { - super(collectionService, folderService, baseStateService); - this.noFolderListSize = 100; - } + async ngOnInit() { + this.searchTypeSearch = !this.platformUtilsService.isSafari(); + this.showLeftHeader = !( + this.popupUtils.inSidebar(window) && this.platformUtilsService.isFirefox() + ); + await this.browserStateService.setBrowserCipherComponentState(null); - async ngOnInit() { - this.searchTypeSearch = !this.platformUtilsService.isSafari(); - this.showLeftHeader = !(this.popupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()); - await this.browserStateService.setBrowserCipherComponentState(null); - - this.broadcasterService.subscribe(ComponentId, (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case 'syncCompleted': - window.setTimeout(() => { - this.load(); - }, 500); - break; - default: - break; - } - - this.changeDetectorRef.detectChanges(); - }); - }); - - const restoredScopeState = await this.restoreState(); - this.route.queryParams.pipe(first()).subscribe(async params => { - this.state = (await this.browserStateService.getBrowserGroupingComponentState()) || {}; - if (this.state?.searchText) { - this.searchText = this.state.searchText; - } else if (params.searchText) { - this.searchText = params.searchText; - this.location.replaceState('vault'); - } - - if (!this.syncService.syncInProgress) { - this.load(); - } else { - this.loadedTimeout = window.setTimeout(() => { - if (!this.loaded) { - this.load(); - } - }, 5000); - } - - if (!this.syncService.syncInProgress || restoredScopeState) { - window.setTimeout(() => this.popupUtils.setContentScrollY(window, this.state?.scrollY), 0); - } - }); - } - - ngOnDestroy() { - if (this.loadedTimeout != null) { - window.clearTimeout(this.loadedTimeout); + this.broadcasterService.subscribe(ComponentId, (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case "syncCompleted": + window.setTimeout(() => { + this.load(); + }, 500); + break; + default: + break; } - if (this.selectedTimeout != null) { - window.clearTimeout(this.selectedTimeout); - } - this.saveState(); - this.broadcasterService.unsubscribe(ComponentId); + + this.changeDetectorRef.detectChanges(); + }); + }); + + const restoredScopeState = await this.restoreState(); + this.route.queryParams.pipe(first()).subscribe(async (params) => { + this.state = (await this.browserStateService.getBrowserGroupingComponentState()) || {}; + if (this.state?.searchText) { + this.searchText = this.state.searchText; + } else if (params.searchText) { + this.searchText = params.searchText; + this.location.replaceState("vault"); + } + + if (!this.syncService.syncInProgress) { + this.load(); + } else { + this.loadedTimeout = window.setTimeout(() => { + if (!this.loaded) { + this.load(); + } + }, 5000); + } + + if (!this.syncService.syncInProgress || restoredScopeState) { + window.setTimeout(() => this.popupUtils.setContentScrollY(window, this.state?.scrollY), 0); + } + }); + } + + ngOnDestroy() { + if (this.loadedTimeout != null) { + window.clearTimeout(this.loadedTimeout); + } + if (this.selectedTimeout != null) { + window.clearTimeout(this.selectedTimeout); + } + this.saveState(); + this.broadcasterService.unsubscribe(ComponentId); + } + + async load() { + await super.load(false); + await this.loadCiphers(); + if (this.showNoFolderCiphers && this.nestedFolders.length > 0) { + // Remove "No Folder" from folder listing + this.nestedFolders = this.nestedFolders.slice(0, this.nestedFolders.length - 1); } - async load() { - await super.load(false); + super.loaded = true; + } + + async loadCiphers() { + this.allCiphers = await this.cipherService.getAllDecrypted(); + if (!this.hasLoadedAllCiphers) { + this.hasLoadedAllCiphers = !this.searchService.isSearchable(this.searchText); + } + this.deletedCount = this.allCiphers.filter((c) => c.isDeleted).length; + await this.search(null); + let favoriteCiphers: CipherView[] = null; + let noFolderCiphers: CipherView[] = null; + const folderCounts = new Map(); + const collectionCounts = new Map(); + const typeCounts = new Map(); + + this.ciphers.forEach((c) => { + if (c.isDeleted) { + return; + } + if (c.favorite) { + if (favoriteCiphers == null) { + favoriteCiphers = []; + } + favoriteCiphers.push(c); + } + + if (c.folderId == null) { + if (noFolderCiphers == null) { + noFolderCiphers = []; + } + noFolderCiphers.push(c); + } + + if (typeCounts.has(c.type)) { + typeCounts.set(c.type, typeCounts.get(c.type) + 1); + } else { + typeCounts.set(c.type, 1); + } + + if (folderCounts.has(c.folderId)) { + folderCounts.set(c.folderId, folderCounts.get(c.folderId) + 1); + } else { + folderCounts.set(c.folderId, 1); + } + + if (c.collectionIds != null) { + c.collectionIds.forEach((colId) => { + if (collectionCounts.has(colId)) { + collectionCounts.set(colId, collectionCounts.get(colId) + 1); + } else { + collectionCounts.set(colId, 1); + } + }); + } + }); + + this.favoriteCiphers = favoriteCiphers; + this.noFolderCiphers = noFolderCiphers; + this.typeCounts = typeCounts; + this.folderCounts = folderCounts; + this.collectionCounts = collectionCounts; + } + + async search(timeout: number = null) { + this.searchPending = false; + if (this.searchTimeout != null) { + clearTimeout(this.searchTimeout); + } + const filterDeleted = (c: CipherView) => !c.isDeleted; + if (timeout == null) { + this.hasSearched = this.searchService.isSearchable(this.searchText); + this.ciphers = await this.searchService.searchCiphers( + this.searchText, + filterDeleted, + this.allCiphers + ); + return; + } + this.searchPending = true; + this.searchTimeout = setTimeout(async () => { + this.hasSearched = this.searchService.isSearchable(this.searchText); + if (!this.hasLoadedAllCiphers && !this.hasSearched) { await this.loadCiphers(); - if (this.showNoFolderCiphers && this.nestedFolders.length > 0) { - // Remove "No Folder" from folder listing - this.nestedFolders = this.nestedFolders.slice(0, this.nestedFolders.length - 1); - } + } else { + this.ciphers = await this.searchService.searchCiphers( + this.searchText, + filterDeleted, + this.allCiphers + ); + } + this.searchPending = false; + }, timeout); + } - super.loaded = true; + async selectType(type: CipherType) { + super.selectType(type); + this.router.navigate(["/ciphers"], { queryParams: { type: type } }); + } + + async selectFolder(folder: FolderView) { + super.selectFolder(folder); + this.router.navigate(["/ciphers"], { queryParams: { folderId: folder.id || "none" } }); + } + + async selectCollection(collection: CollectionView) { + super.selectCollection(collection); + this.router.navigate(["/ciphers"], { queryParams: { collectionId: collection.id } }); + } + + async selectTrash() { + super.selectTrash(); + this.router.navigate(["/ciphers"], { queryParams: { deleted: true } }); + } + + async selectCipher(cipher: CipherView) { + this.selectedTimeout = window.setTimeout(() => { + if (!this.preventSelected) { + this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } }); + } + this.preventSelected = false; + }, 200); + } + + async launchCipher(cipher: CipherView) { + if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) { + return; } - async loadCiphers() { - this.allCiphers = await this.cipherService.getAllDecrypted(); - if (!this.hasLoadedAllCiphers) { - this.hasLoadedAllCiphers = !this.searchService.isSearchable(this.searchText); - } - this.deletedCount = this.allCiphers.filter(c => c.isDeleted).length; - await this.search(null); - let favoriteCiphers: CipherView[] = null; - let noFolderCiphers: CipherView[] = null; - const folderCounts = new Map(); - const collectionCounts = new Map(); - const typeCounts = new Map(); + if (this.selectedTimeout != null) { + window.clearTimeout(this.selectedTimeout); + } + this.preventSelected = true; + await this.cipherService.updateLastLaunchedDate(cipher.id); + BrowserApi.createNewTab(cipher.login.launchUri); + if (this.popupUtils.inPopup(window)) { + BrowserApi.closePopup(window); + } + } - this.ciphers.forEach(c => { - if (c.isDeleted) { - return; - } - if (c.favorite) { - if (favoriteCiphers == null) { - favoriteCiphers = []; - } - favoriteCiphers.push(c); - } + async addCipher() { + this.router.navigate(["/add-cipher"]); + } - if (c.folderId == null) { - if (noFolderCiphers == null) { - noFolderCiphers = []; - } - noFolderCiphers.push(c); - } + showSearching() { + return ( + this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText)) + ); + } - if (typeCounts.has(c.type)) { - typeCounts.set(c.type, typeCounts.get(c.type) + 1); - } else { - typeCounts.set(c.type, 1); - } + closeOnEsc(e: KeyboardEvent) { + // If input not empty, use browser default behavior of clearing input instead + if (e.key === "Escape" && (this.searchText == null || this.searchText === "")) { + BrowserApi.closePopup(window); + } + } - if (folderCounts.has(c.folderId)) { - folderCounts.set(c.folderId, folderCounts.get(c.folderId) + 1); - } else { - folderCounts.set(c.folderId, 1); - } + private async saveState() { + this.state = { + scrollY: this.popupUtils.getContentScrollY(window), + searchText: this.searchText, + favoriteCiphers: this.favoriteCiphers, + noFolderCiphers: this.noFolderCiphers, + ciphers: this.ciphers, + collectionCounts: this.collectionCounts, + folderCounts: this.folderCounts, + typeCounts: this.typeCounts, + folders: this.folders, + collections: this.collections, + deletedCount: this.deletedCount, + }; + await this.browserStateService.setBrowserGroupingComponentState(this.scopeState); + } - if (c.collectionIds != null) { - c.collectionIds.forEach(colId => { - if (collectionCounts.has(colId)) { - collectionCounts.set(colId, collectionCounts.get(colId) + 1); - } else { - collectionCounts.set(colId, 1); - } - }); - } - }); - - this.favoriteCiphers = favoriteCiphers; - this.noFolderCiphers = noFolderCiphers; - this.typeCounts = typeCounts; - this.folderCounts = folderCounts; - this.collectionCounts = collectionCounts; + private async restoreState(): Promise { + this.scopeState = await this.browserStateService.getBrowserGroupingComponentState(); + if (this.scopeState == null) { + return false; } - async search(timeout: number = null) { - this.searchPending = false; - if (this.searchTimeout != null) { - clearTimeout(this.searchTimeout); - } - const filterDeleted = (c: CipherView) => !c.isDeleted; - if (timeout == null) { - this.hasSearched = this.searchService.isSearchable(this.searchText); - this.ciphers = await this.searchService.searchCiphers(this.searchText, filterDeleted, this.allCiphers); - return; - } - this.searchPending = true; - this.searchTimeout = setTimeout(async () => { - this.hasSearched = this.searchService.isSearchable(this.searchText); - if (!this.hasLoadedAllCiphers && !this.hasSearched) { - await this.loadCiphers(); - } else { - this.ciphers = await this.searchService.searchCiphers(this.searchText, filterDeleted, this.allCiphers); - } - this.searchPending = false; - }, timeout); + if (this.scopeState.favoriteCiphers != null) { + this.favoriteCiphers = this.scopeState.favoriteCiphers; + } + if (this.scopeState.noFolderCiphers != null) { + this.noFolderCiphers = this.scopeState.noFolderCiphers; + } + if (this.scopeState.ciphers != null) { + this.ciphers = this.scopeState.ciphers; + } + if (this.scopeState.collectionCounts != null) { + this.collectionCounts = this.scopeState.collectionCounts; + } + if (this.scopeState.folderCounts != null) { + this.folderCounts = this.scopeState.folderCounts; + } + if (this.scopeState.typeCounts != null) { + this.typeCounts = this.scopeState.typeCounts; + } + if (this.scopeState.folders != null) { + this.folders = this.scopeState.folders; + } + if (this.scopeState.collections != null) { + this.collections = this.scopeState.collections; + } + if (this.scopeState.deletedCiphers != null) { + this.deletedCount = this.scopeState.deletedCount; } - async selectType(type: CipherType) { - super.selectType(type); - this.router.navigate(['/ciphers'], { queryParams: { type: type } }); - } - - async selectFolder(folder: FolderView) { - super.selectFolder(folder); - this.router.navigate(['/ciphers'], { queryParams: { folderId: folder.id || 'none' } }); - } - - async selectCollection(collection: CollectionView) { - super.selectCollection(collection); - this.router.navigate(['/ciphers'], { queryParams: { collectionId: collection.id } }); - } - - async selectTrash() { - super.selectTrash(); - this.router.navigate(['/ciphers'], { queryParams: { deleted: true } }); - } - - async selectCipher(cipher: CipherView) { - this.selectedTimeout = window.setTimeout(() => { - if (!this.preventSelected) { - this.router.navigate(['/view-cipher'], { queryParams: { cipherId: cipher.id } }); - } - this.preventSelected = false; - }, 200); - } - - async launchCipher(cipher: CipherView) { - if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) { - return; - } - - if (this.selectedTimeout != null) { - window.clearTimeout(this.selectedTimeout); - } - this.preventSelected = true; - await this.cipherService.updateLastLaunchedDate(cipher.id); - BrowserApi.createNewTab(cipher.login.launchUri); - if (this.popupUtils.inPopup(window)) { - BrowserApi.closePopup(window); - } - } - - async addCipher() { - this.router.navigate(['/add-cipher']); - } - - showSearching() { - return this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText)); - } - - closeOnEsc(e: KeyboardEvent) { - // If input not empty, use browser default behavior of clearing input instead - if (e.key === 'Escape' && (this.searchText == null || this.searchText === '')) { - BrowserApi.closePopup(window); - } - } - - private async saveState() { - this.state = { - scrollY: this.popupUtils.getContentScrollY(window), - searchText: this.searchText, - favoriteCiphers: this.favoriteCiphers, - noFolderCiphers: this.noFolderCiphers, - ciphers: this.ciphers, - collectionCounts: this.collectionCounts, - folderCounts: this.folderCounts, - typeCounts: this.typeCounts, - folders: this.folders, - collections: this.collections, - deletedCount: this.deletedCount, - }; - await this.browserStateService.setBrowserGroupingComponentState(this.scopeState); - } - - private async restoreState(): Promise { - this.scopeState = await this.browserStateService.getBrowserGroupingComponentState(); - if (this.scopeState == null) { - return false; - } - - if (this.scopeState.favoriteCiphers != null) { - this.favoriteCiphers = this.scopeState.favoriteCiphers; - } - if (this.scopeState.noFolderCiphers != null) { - this.noFolderCiphers = this.scopeState.noFolderCiphers; - } - if (this.scopeState.ciphers != null) { - this.ciphers = this.scopeState.ciphers; - } - if (this.scopeState.collectionCounts != null) { - this.collectionCounts = this.scopeState.collectionCounts; - } - if (this.scopeState.folderCounts != null) { - this.folderCounts = this.scopeState.folderCounts; - } - if (this.scopeState.typeCounts != null) { - this.typeCounts = this.scopeState.typeCounts; - } - if (this.scopeState.folders != null) { - this.folders = this.scopeState.folders; - } - if (this.scopeState.collections != null) { - this.collections = this.scopeState.collections; - } - if (this.scopeState.deletedCiphers != null) { - this.deletedCount = this.scopeState.deletedCount; - } - - return true; - } + return true; + } } diff --git a/src/popup/vault/share.component.ts b/src/popup/vault/share.component.ts index 92d928b62d..3c34562fc1 100644 --- a/src/popup/vault/share.component.ts +++ b/src/popup/vault/share.component.ts @@ -1,51 +1,63 @@ -import { Component } from '@angular/core'; -import { - ActivatedRoute, - Router, -} from '@angular/router'; +import { Component } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; -import { first } from 'rxjs/operators'; +import { first } from "rxjs/operators"; -import { CipherService } from 'jslib-common/abstractions/cipher.service'; -import { CollectionService } from 'jslib-common/abstractions/collection.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { OrganizationService } from 'jslib-common/abstractions/organization.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { CipherService } from "jslib-common/abstractions/cipher.service"; +import { CollectionService } from "jslib-common/abstractions/collection.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { OrganizationService } from "jslib-common/abstractions/organization.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; -import { ShareComponent as BaseShareComponent } from 'jslib-angular/components/share.component'; +import { ShareComponent as BaseShareComponent } from "jslib-angular/components/share.component"; @Component({ - selector: 'app-vault-share', - templateUrl: 'share.component.html', + selector: "app-vault-share", + templateUrl: "share.component.html", }) export class ShareComponent extends BaseShareComponent { - constructor(collectionService: CollectionService, platformUtilsService: PlatformUtilsService, - i18nService: I18nService, logService: LogService, - cipherService: CipherService, private route: ActivatedRoute, - private router: Router, organizationService: OrganizationService) { - super(collectionService, platformUtilsService, i18nService, cipherService, - logService, organizationService); - } + constructor( + collectionService: CollectionService, + platformUtilsService: PlatformUtilsService, + i18nService: I18nService, + logService: LogService, + cipherService: CipherService, + private route: ActivatedRoute, + private router: Router, + organizationService: OrganizationService + ) { + super( + collectionService, + platformUtilsService, + i18nService, + cipherService, + logService, + organizationService + ); + } - async ngOnInit() { - this.onSharedCipher.subscribe(() => { - this.router.navigate(['view-cipher', { cipherId: this.cipherId }]); - }); - this.route.queryParams.pipe(first()).subscribe(async params => { - this.cipherId = params.cipherId; - await this.load(); - }); - } + async ngOnInit() { + this.onSharedCipher.subscribe(() => { + this.router.navigate(["view-cipher", { cipherId: this.cipherId }]); + }); + this.route.queryParams.pipe(first()).subscribe(async (params) => { + this.cipherId = params.cipherId; + await this.load(); + }); + } - async submit(): Promise { - const success = await super.submit(); - if (success) { - this.cancel(); - } - return success; + async submit(): Promise { + const success = await super.submit(); + if (success) { + this.cancel(); } + return success; + } - cancel() { - this.router.navigate(['/view-cipher'], { replaceUrl: true, queryParams: { cipherId: this.cipher.id } }); - } + cancel() { + this.router.navigate(["/view-cipher"], { + replaceUrl: true, + queryParams: { cipherId: this.cipher.id }, + }); + } } diff --git a/src/popup/vault/view.component.ts b/src/popup/vault/view.component.ts index cab6beaf8b..07f41adf9e 100644 --- a/src/popup/vault/view.component.ts +++ b/src/popup/vault/view.component.ts @@ -1,273 +1,297 @@ -import { Location } from '@angular/common'; -import { - ChangeDetectorRef, - Component, - NgZone, -} from '@angular/core'; -import { - ActivatedRoute, - Router, -} from '@angular/router'; +import { Location } from "@angular/common"; +import { ChangeDetectorRef, Component, NgZone } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; -import { first } from 'rxjs/operators'; +import { first } from "rxjs/operators"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { AuditService } from 'jslib-common/abstractions/audit.service'; -import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; -import { CipherService } from 'jslib-common/abstractions/cipher.service'; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { EventService } from 'jslib-common/abstractions/event.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { TokenService } from 'jslib-common/abstractions/token.service'; -import { TotpService } from 'jslib-common/abstractions/totp.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { AuditService } from "jslib-common/abstractions/audit.service"; +import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; +import { CipherService } from "jslib-common/abstractions/cipher.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { EventService } from "jslib-common/abstractions/event.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { TokenService } from "jslib-common/abstractions/token.service"; +import { TotpService } from "jslib-common/abstractions/totp.service"; -import { Cipher } from 'jslib-common/models/domain/cipher'; -import { LoginUriView } from 'jslib-common/models/view/loginUriView'; +import { Cipher } from "jslib-common/models/domain/cipher"; +import { LoginUriView } from "jslib-common/models/view/loginUriView"; -import { CipherType } from 'jslib-common/enums/cipherType'; +import { CipherType } from "jslib-common/enums/cipherType"; -import { ViewComponent as BaseViewComponent } from 'jslib-angular/components/view.component'; +import { ViewComponent as BaseViewComponent } from "jslib-angular/components/view.component"; -import { BrowserApi } from '../../browser/browserApi'; +import { BrowserApi } from "../../browser/browserApi"; -import { AutofillService } from '../../services/abstractions/autofill.service'; -import { StateService } from '../../services/abstractions/state.service'; +import { AutofillService } from "../../services/abstractions/autofill.service"; +import { StateService } from "../../services/abstractions/state.service"; -import { PopupUtilsService } from '../services/popup-utils.service'; +import { PopupUtilsService } from "../services/popup-utils.service"; -const BroadcasterSubscriptionId = 'ChildViewComponent'; +const BroadcasterSubscriptionId = "ChildViewComponent"; @Component({ - selector: 'app-vault-view', - templateUrl: 'view.component.html', + selector: "app-vault-view", + templateUrl: "view.component.html", }) export class ViewComponent extends BaseViewComponent { - showAttachments = true; - pageDetails: any[] = []; - tab: any; - loadPageDetailsTimeout: number; - inPopout = false; - cipherType = CipherType; + showAttachments = true; + pageDetails: any[] = []; + tab: any; + loadPageDetailsTimeout: number; + inPopout = false; + cipherType = CipherType; - constructor(cipherService: CipherService, totpService: TotpService, - tokenService: TokenService, i18nService: I18nService, - cryptoService: CryptoService, platformUtilsService: PlatformUtilsService, - auditService: AuditService, private route: ActivatedRoute, - private router: Router, private location: Location, - broadcasterService: BroadcasterService, ngZone: NgZone, - changeDetectorRef: ChangeDetectorRef, stateService: StateService, - eventService: EventService, private autofillService: AutofillService, - private messagingService: MessagingService, private popupUtilsService: PopupUtilsService, - apiService: ApiService, passwordRepromptService: PasswordRepromptService, - logService: LogService) { - super(cipherService, totpService, tokenService, i18nService, - cryptoService, platformUtilsService, auditService, window, - broadcasterService, ngZone, changeDetectorRef, eventService, - apiService, passwordRepromptService, logService, stateService); - } + constructor( + cipherService: CipherService, + totpService: TotpService, + tokenService: TokenService, + i18nService: I18nService, + cryptoService: CryptoService, + platformUtilsService: PlatformUtilsService, + auditService: AuditService, + private route: ActivatedRoute, + private router: Router, + private location: Location, + broadcasterService: BroadcasterService, + ngZone: NgZone, + changeDetectorRef: ChangeDetectorRef, + stateService: StateService, + eventService: EventService, + private autofillService: AutofillService, + private messagingService: MessagingService, + private popupUtilsService: PopupUtilsService, + apiService: ApiService, + passwordRepromptService: PasswordRepromptService, + logService: LogService + ) { + super( + cipherService, + totpService, + tokenService, + i18nService, + cryptoService, + platformUtilsService, + auditService, + window, + broadcasterService, + ngZone, + changeDetectorRef, + eventService, + apiService, + passwordRepromptService, + logService, + stateService + ); + } - ngOnInit() { - this.inPopout = this.popupUtilsService.inPopout(window); - this.route.queryParams.pipe(first()).subscribe(async params => { - if (params.cipherId) { - this.cipherId = params.cipherId; - } else { - this.close(); + ngOnInit() { + this.inPopout = this.popupUtilsService.inPopout(window); + this.route.queryParams.pipe(first()).subscribe(async (params) => { + if (params.cipherId) { + this.cipherId = params.cipherId; + } else { + this.close(); + } + + await this.load(); + }); + + super.ngOnInit(); + + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case "collectPageDetailsResponse": + if (message.sender === BroadcasterSubscriptionId) { + this.pageDetails.push({ + frameId: message.webExtSender.frameId, + tab: message.tab, + details: message.details, + }); } - - await this.load(); - }); - - super.ngOnInit(); - - this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case 'collectPageDetailsResponse': - if (message.sender === BroadcasterSubscriptionId) { - this.pageDetails.push({ - frameId: message.webExtSender.frameId, - tab: message.tab, - details: message.details, - }); - } - break; - case 'tabChanged': - case 'windowChanged': - if (this.loadPageDetailsTimeout != null) { - window.clearTimeout(this.loadPageDetailsTimeout); - } - this.loadPageDetailsTimeout = window.setTimeout(() => this.loadPageDetails(), 500); - break; - default: - break; - } - }); - }); - } - - ngOnDestroy() { - super.ngOnDestroy(); - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - } - - async load() { - await super.load(); - await this.loadPageDetails(); - } - - async edit() { - if (this.cipher.isDeleted) { - return false; - } - if (!await super.edit()) { - return false; - } - - this.router.navigate(['/edit-cipher'], { queryParams: { cipherId: this.cipher.id } }); - return true; - } - - async clone() { - if (this.cipher.isDeleted) { - return false; - } - - if (!await super.clone()) { - return false; - } - - this.router.navigate(['/clone-cipher'], { - queryParams: { - cloneMode: true, - cipherId: this.cipher.id, - }, - }); - return true; - } - - async share() { - if (!await super.share()) { - return false; - } - - if (this.cipher.organizationId == null) { - this.router.navigate(['/share-cipher'], { replaceUrl: true, queryParams: { cipherId: this.cipher.id } }); - } - return true; - } - - async fillCipher() { - const didAutofill = await this.doAutofill(); - if (didAutofill) { - this.platformUtilsService.showToast('success', null, - this.i18nService.t('autoFillSuccess')); - } - } - - async fillCipherAndSave() { - const didAutofill = await this.doAutofill(); - - if (didAutofill) { - if (this.tab == null) { - throw new Error('No tab found.'); - } - - if (this.cipher.login.uris == null) { - this.cipher.login.uris = []; - } else { - if (this.cipher.login.uris.some(uri => uri.uri === this.tab.url)) { - this.platformUtilsService.showToast('success', null, - this.i18nService.t('autoFillSuccessAndSavedUri')); - return; - } - } - - const loginUri = new LoginUriView(); - loginUri.uri = this.tab.url; - this.cipher.login.uris.push(loginUri); - - try { - const cipher: Cipher = await this.cipherService.encrypt(this.cipher); - await this.cipherService.saveWithServer(cipher); - this.platformUtilsService.showToast('success', null, - this.i18nService.t('autoFillSuccessAndSavedUri')); - this.messagingService.send('editedCipher'); - } catch { - this.platformUtilsService.showToast('error', null, - this.i18nService.t('unexpectedError')); + break; + case "tabChanged": + case "windowChanged": + if (this.loadPageDetailsTimeout != null) { + window.clearTimeout(this.loadPageDetailsTimeout); } + this.loadPageDetailsTimeout = window.setTimeout(() => this.loadPageDetails(), 500); + break; + default: + break; } + }); + }); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + } + + async load() { + await super.load(); + await this.loadPageDetails(); + } + + async edit() { + if (this.cipher.isDeleted) { + return false; + } + if (!(await super.edit())) { + return false; } - async restore() { - if (!this.cipher.isDeleted) { - return false; - } - if (await super.restore()) { - this.close(); - return true; - } - return false; + this.router.navigate(["/edit-cipher"], { queryParams: { cipherId: this.cipher.id } }); + return true; + } + + async clone() { + if (this.cipher.isDeleted) { + return false; } - async delete() { - if (await super.delete()) { - this.close(); - return true; - } - return false; + if (!(await super.clone())) { + return false; } - close() { - this.location.back(); + this.router.navigate(["/clone-cipher"], { + queryParams: { + cloneMode: true, + cipherId: this.cipher.id, + }, + }); + return true; + } + + async share() { + if (!(await super.share())) { + return false; } - private async loadPageDetails() { - this.pageDetails = []; - this.tab = await BrowserApi.getTabFromCurrentWindow(); - if (this.tab == null) { - return; + if (this.cipher.organizationId == null) { + this.router.navigate(["/share-cipher"], { + replaceUrl: true, + queryParams: { cipherId: this.cipher.id }, + }); + } + return true; + } + + async fillCipher() { + const didAutofill = await this.doAutofill(); + if (didAutofill) { + this.platformUtilsService.showToast("success", null, this.i18nService.t("autoFillSuccess")); + } + } + + async fillCipherAndSave() { + const didAutofill = await this.doAutofill(); + + if (didAutofill) { + if (this.tab == null) { + throw new Error("No tab found."); + } + + if (this.cipher.login.uris == null) { + this.cipher.login.uris = []; + } else { + if (this.cipher.login.uris.some((uri) => uri.uri === this.tab.url)) { + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("autoFillSuccessAndSavedUri") + ); + return; } - BrowserApi.tabSendMessage(this.tab, { - command: 'collectPageDetails', - tab: this.tab, - sender: BroadcasterSubscriptionId, - }); + } + + const loginUri = new LoginUriView(); + loginUri.uri = this.tab.url; + this.cipher.login.uris.push(loginUri); + + try { + const cipher: Cipher = await this.cipherService.encrypt(this.cipher); + await this.cipherService.saveWithServer(cipher); + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("autoFillSuccessAndSavedUri") + ); + this.messagingService.send("editedCipher"); + } catch { + this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError")); + } + } + } + + async restore() { + if (!this.cipher.isDeleted) { + return false; + } + if (await super.restore()) { + this.close(); + return true; + } + return false; + } + + async delete() { + if (await super.delete()) { + this.close(); + return true; + } + return false; + } + + close() { + this.location.back(); + } + + private async loadPageDetails() { + this.pageDetails = []; + this.tab = await BrowserApi.getTabFromCurrentWindow(); + if (this.tab == null) { + return; + } + BrowserApi.tabSendMessage(this.tab, { + command: "collectPageDetails", + tab: this.tab, + sender: BroadcasterSubscriptionId, + }); + } + + private async doAutofill() { + if (!(await this.promptPassword())) { + return false; } - private async doAutofill() { - if (!await this.promptPassword()) { - return false; - } - - if (this.pageDetails == null || this.pageDetails.length === 0) { - this.platformUtilsService.showToast('error', null, - this.i18nService.t('autofillError')); - return false; - } - - try { - this.totpCode = await this.autofillService.doAutoFill({ - cipher: this.cipher, - pageDetails: this.pageDetails, - doc: window.document, - fillNewPassword: true, - }); - if (this.totpCode != null) { - this.platformUtilsService.copyToClipboard(this.totpCode, { window: window }); - } - } catch { - this.platformUtilsService.showToast('error', null, - this.i18nService.t('autofillError')); - this.changeDetectorRef.detectChanges(); - return false; - } - - return true; + if (this.pageDetails == null || this.pageDetails.length === 0) { + this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError")); + return false; } + + try { + this.totpCode = await this.autofillService.doAutoFill({ + cipher: this.cipher, + pageDetails: this.pageDetails, + doc: window.document, + fillNewPassword: true, + }); + if (this.totpCode != null) { + this.platformUtilsService.copyToClipboard(this.totpCode, { window: window }); + } + } catch { + this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError")); + this.changeDetectorRef.detectChanges(); + return false; + } + + return true; + } } diff --git a/src/services/abstractions/state.service.ts b/src/services/abstractions/state.service.ts index 5209cb3c73..1cf50c9048 100644 --- a/src/services/abstractions/state.service.ts +++ b/src/services/abstractions/state.service.ts @@ -1,20 +1,33 @@ -import { StateService as BaseStateServiceAbstraction } from 'jslib-common/abstractions/state.service'; +import { StateService as BaseStateServiceAbstraction } from "jslib-common/abstractions/state.service"; -import { StorageOptions } from 'jslib-common/models/domain/storageOptions'; - -import { Account } from 'src/models/account'; -import { BrowserComponentState } from 'src/models/browserComponentState'; -import { BrowserGroupingsComponentState } from 'src/models/browserGroupingsComponentState'; -import { BrowserSendComponentState } from 'src/models/browserSendComponentState'; +import { StorageOptions } from "jslib-common/models/domain/storageOptions"; +import { Account } from "src/models/account"; +import { BrowserComponentState } from "src/models/browserComponentState"; +import { BrowserGroupingsComponentState } from "src/models/browserGroupingsComponentState"; +import { BrowserSendComponentState } from "src/models/browserSendComponentState"; export abstract class StateService extends BaseStateServiceAbstraction { - getBrowserGroupingComponentState: (options?: StorageOptions) => Promise; - setBrowserGroupingComponentState: (value: BrowserGroupingsComponentState, options?: StorageOptions) => Promise; - getBrowserCipherComponentState: (options?: StorageOptions) => Promise; - setBrowserCipherComponentState: (value: BrowserComponentState, options?: StorageOptions) => Promise; - getBrowserSendComponentState: (options?: StorageOptions) => Promise; - setBrowserSendComponentState: (value: BrowserSendComponentState, options?: StorageOptions) => Promise; - getBrowserSendTypeComponentState: (options?: StorageOptions) => Promise; - setBrowserSendTypeComponentState: (value: BrowserComponentState, options?: StorageOptions) => Promise; + getBrowserGroupingComponentState: ( + options?: StorageOptions + ) => Promise; + setBrowserGroupingComponentState: ( + value: BrowserGroupingsComponentState, + options?: StorageOptions + ) => Promise; + getBrowserCipherComponentState: (options?: StorageOptions) => Promise; + setBrowserCipherComponentState: ( + value: BrowserComponentState, + options?: StorageOptions + ) => Promise; + getBrowserSendComponentState: (options?: StorageOptions) => Promise; + setBrowserSendComponentState: ( + value: BrowserSendComponentState, + options?: StorageOptions + ) => Promise; + getBrowserSendTypeComponentState: (options?: StorageOptions) => Promise; + setBrowserSendTypeComponentState: ( + value: BrowserComponentState, + options?: StorageOptions + ) => Promise; } diff --git a/src/services/browserCrypto.service.ts b/src/services/browserCrypto.service.ts index b9bb4d0585..64c4c9ca9e 100644 --- a/src/services/browserCrypto.service.ts +++ b/src/services/browserCrypto.service.ts @@ -1,14 +1,14 @@ -import { KeySuffixOptions } from 'jslib-common/enums/keySuffixOptions'; +import { KeySuffixOptions } from "jslib-common/enums/keySuffixOptions"; -import { CryptoService } from 'jslib-common/services/crypto.service'; +import { CryptoService } from "jslib-common/services/crypto.service"; export class BrowserCryptoService extends CryptoService { - protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions) { - if (keySuffix === 'biometric') { - await this.platformUtilService.authenticateBiometric(); - return (await this.getKey())?.keyB64; - } - - return await super.retrieveKeyFromStorage(keySuffix); + protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions) { + if (keySuffix === "biometric") { + await this.platformUtilService.authenticateBiometric(); + return (await this.getKey())?.keyB64; } + + return await super.retrieveKeyFromStorage(keySuffix); + } } diff --git a/src/services/browserPlatformUtils.service.ts b/src/services/browserPlatformUtils.service.ts index f8c4ad2174..d6db83865b 100644 --- a/src/services/browserPlatformUtils.service.ts +++ b/src/services/browserPlatformUtils.service.ts @@ -1,340 +1,375 @@ -import { BrowserApi } from '../browser/browserApi'; -import { SafariApp } from '../browser/safariApp'; +import { BrowserApi } from "../browser/browserApi"; +import { SafariApp } from "../browser/safariApp"; -import { DeviceType } from 'jslib-common/enums/deviceType'; -import { ThemeType } from 'jslib-common/enums/themeType'; +import { DeviceType } from "jslib-common/enums/deviceType"; +import { ThemeType } from "jslib-common/enums/themeType"; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StateService } from '../services/abstractions/state.service'; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "../services/abstractions/state.service"; const DialogPromiseExpiration = 600000; // 10 minutes export default class BrowserPlatformUtilsService implements PlatformUtilsService { - identityClientId: string = 'browser'; + identityClientId: string = "browser"; - private showDialogResolves = new Map void, date: Date }>(); - private passwordDialogResolves = new Map Promise, date: Date }>(); - private deviceCache: DeviceType = null; - private prefersColorSchemeDark = window.matchMedia('(prefers-color-scheme: dark)'); + private showDialogResolves = new Map void; date: Date }>(); + private passwordDialogResolves = new Map< + number, + { tryResolve: (canceled: boolean, password: string) => Promise; date: Date } + >(); + private deviceCache: DeviceType = null; + private prefersColorSchemeDark = window.matchMedia("(prefers-color-scheme: dark)"); - constructor(private messagingService: MessagingService, private stateService: StateService, - private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, - private biometricCallback: () => Promise) { } + constructor( + private messagingService: MessagingService, + private stateService: StateService, + private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, + private biometricCallback: () => Promise + ) {} - getDevice(): DeviceType { - if (this.deviceCache) { - return this.deviceCache; + getDevice(): DeviceType { + if (this.deviceCache) { + return this.deviceCache; + } + + if ( + navigator.userAgent.indexOf(" Firefox/") !== -1 || + navigator.userAgent.indexOf(" Gecko/") !== -1 + ) { + this.deviceCache = DeviceType.FirefoxExtension; + } else if ( + (!!(window as any).opr && !!opr.addons) || + !!(window as any).opera || + navigator.userAgent.indexOf(" OPR/") >= 0 + ) { + this.deviceCache = DeviceType.OperaExtension; + } else if (navigator.userAgent.indexOf(" Edg/") !== -1) { + this.deviceCache = DeviceType.EdgeExtension; + } else if (navigator.userAgent.indexOf(" Vivaldi/") !== -1) { + this.deviceCache = DeviceType.VivaldiExtension; + } else if ((window as any).chrome && navigator.userAgent.indexOf(" Chrome/") !== -1) { + this.deviceCache = DeviceType.ChromeExtension; + } else if (navigator.userAgent.indexOf(" Safari/") !== -1) { + this.deviceCache = DeviceType.SafariExtension; + } + + return this.deviceCache; + } + + getDeviceString(): string { + const device = DeviceType[this.getDevice()].toLowerCase(); + return device.replace("extension", ""); + } + + isFirefox(): boolean { + return this.getDevice() === DeviceType.FirefoxExtension; + } + + isChrome(): boolean { + return this.getDevice() === DeviceType.ChromeExtension; + } + + isEdge(): boolean { + return this.getDevice() === DeviceType.EdgeExtension; + } + + isOpera(): boolean { + return this.getDevice() === DeviceType.OperaExtension; + } + + isVivaldi(): boolean { + return this.getDevice() === DeviceType.VivaldiExtension; + } + + isSafari(): boolean { + return this.getDevice() === DeviceType.SafariExtension; + } + + isIE(): boolean { + return false; + } + + isMacAppStore(): boolean { + return false; + } + + async isViewOpen(): Promise { + if (await BrowserApi.isPopupOpen()) { + return true; + } + + if (this.isSafari()) { + return false; + } + + const sidebarView = this.sidebarViewName(); + const sidebarOpen = + sidebarView != null && chrome.extension.getViews({ type: sidebarView }).length > 0; + if (sidebarOpen) { + return true; + } + + const tabOpen = chrome.extension.getViews({ type: "tab" }).length > 0; + return tabOpen; + } + + lockTimeout(): number { + return null; + } + + launchUri(uri: string, options?: any): void { + 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()); + } + + supportsWebAuthn(win: Window): boolean { + return typeof PublicKeyCredential !== "undefined"; + } + + supportsDuo(): boolean { + return true; + } + + showToast( + type: "error" | "success" | "warning" | "info", + title: string, + text: string | string[], + options?: any + ): void { + this.messagingService.send("showToast", { + text: text, + title: title, + type: type, + options: options, + }); + } + + showDialog( + body: string, + title?: string, + confirmText?: string, + cancelText?: string, + type?: string, + bodyIsHtml: boolean = false + ) { + const dialogId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); + this.messagingService.send("showDialog", { + text: bodyIsHtml ? null : body, + html: bodyIsHtml ? body : null, + title: title, + confirmText: confirmText, + cancelText: cancelText, + type: type, + dialogId: dialogId, + }); + return new Promise((resolve) => { + this.showDialogResolves.set(dialogId, { resolve: resolve, date: new Date() }); + }); + } + + isDev(): boolean { + return process.env.ENV === "development"; + } + + isSelfHost(): boolean { + return false; + } + + copyToClipboard(text: string, options?: any): void { + let win = window; + let doc = window.document; + if (options && (options.window || options.win)) { + win = options.window || options.win; + doc = win.document; + } else if (options && options.doc) { + doc = options.doc; + } + const clearing = options ? !!options.clearing : false; + const clearMs: number = options && options.clearMs ? options.clearMs : null; + + if (this.isSafari()) { + SafariApp.sendMessageToApp("copyToClipboard", text).then(() => { + if (!clearing && this.clipboardWriteCallback != null) { + this.clipboardWriteCallback(text, clearMs); } - - if (navigator.userAgent.indexOf(' Firefox/') !== -1 || navigator.userAgent.indexOf(' Gecko/') !== -1) { - this.deviceCache = DeviceType.FirefoxExtension; - } else if ((!!(window as any).opr && !!opr.addons) || !!(window as any).opera || - navigator.userAgent.indexOf(' OPR/') >= 0) { - this.deviceCache = DeviceType.OperaExtension; - } else if (navigator.userAgent.indexOf(' Edg/') !== -1) { - this.deviceCache = DeviceType.EdgeExtension; - } else if (navigator.userAgent.indexOf(' Vivaldi/') !== -1) { - this.deviceCache = DeviceType.VivaldiExtension; - } else if ((window as any).chrome && navigator.userAgent.indexOf(' Chrome/') !== -1) { - this.deviceCache = DeviceType.ChromeExtension; - } else if (navigator.userAgent.indexOf(' Safari/') !== -1) { - this.deviceCache = DeviceType.SafariExtension; + }); + } else if ( + this.isFirefox() && + (win as any).navigator.clipboard && + (win as any).navigator.clipboard.writeText + ) { + (win as any).navigator.clipboard.writeText(text).then(() => { + if (!clearing && this.clipboardWriteCallback != null) { + this.clipboardWriteCallback(text, clearMs); } + }); + } else if ((win as any).clipboardData && (win as any).clipboardData.setData) { + // IE specific code path to prevent textarea being shown while dialog is visible. + (win as any).clipboardData.setData("Text", text); + if (!clearing && this.clipboardWriteCallback != null) { + this.clipboardWriteCallback(text, clearMs); + } + } else if (doc.queryCommandSupported && doc.queryCommandSupported("copy")) { + if (this.isChrome() && text === "") { + text = "\u0000"; + } - return this.deviceCache; - } + const textarea = doc.createElement("textarea"); + textarea.textContent = text == null || text === "" ? " " : text; + // Prevent scrolling to bottom of page in MS Edge. + textarea.style.position = "fixed"; + doc.body.appendChild(textarea); + textarea.select(); - getDeviceString(): string { - const device = DeviceType[this.getDevice()].toLowerCase(); - return device.replace('extension', ''); - } - - isFirefox(): boolean { - return this.getDevice() === DeviceType.FirefoxExtension; - } - - isChrome(): boolean { - return this.getDevice() === DeviceType.ChromeExtension; - } - - isEdge(): boolean { - return this.getDevice() === DeviceType.EdgeExtension; - } - - isOpera(): boolean { - return this.getDevice() === DeviceType.OperaExtension; - } - - isVivaldi(): boolean { - return this.getDevice() === DeviceType.VivaldiExtension; - } - - isSafari(): boolean { - return this.getDevice() === DeviceType.SafariExtension; - } - - isIE(): boolean { - return false; - } - - isMacAppStore(): boolean { - return false; - } - - async isViewOpen(): Promise { - if (await BrowserApi.isPopupOpen()) { - return true; + try { + // Security exception may be thrown by some browsers. + if (doc.execCommand("copy") && !clearing && this.clipboardWriteCallback != null) { + this.clipboardWriteCallback(text, clearMs); } + } catch (e) { + // tslint:disable-next-line + console.warn("Copy to clipboard failed.", e); + } finally { + doc.body.removeChild(textarea); + } + } + } - if (this.isSafari()) { - return false; + async readFromClipboard(options?: any): Promise { + let win = window; + let doc = window.document; + if (options && (options.window || options.win)) { + win = options.window || options.win; + doc = win.document; + } else if (options && options.doc) { + doc = options.doc; + } + + if (this.isSafari()) { + return await SafariApp.sendMessageToApp("readFromClipboard"); + } else if ( + this.isFirefox() && + (win as any).navigator.clipboard && + (win as any).navigator.clipboard.readText + ) { + return await (win as any).navigator.clipboard.readText(); + } else if (doc.queryCommandSupported && doc.queryCommandSupported("paste")) { + const textarea = doc.createElement("textarea"); + // Prevent scrolling to bottom of page in MS Edge. + textarea.style.position = "fixed"; + doc.body.appendChild(textarea); + textarea.focus(); + try { + // Security exception may be thrown by some browsers. + if (doc.execCommand("paste")) { + return textarea.value; } + } catch (e) { + // tslint:disable-next-line + console.warn("Read from clipboard failed.", e); + } finally { + doc.body.removeChild(textarea); + } + } + return null; + } - const sidebarView = this.sidebarViewName(); - const sidebarOpen = sidebarView != null && chrome.extension.getViews({ type: sidebarView }).length > 0; - if (sidebarOpen) { - return true; - } - - const tabOpen = chrome.extension.getViews({ type: 'tab' }).length > 0; - return tabOpen; + resolveDialogPromise(dialogId: number, confirmed: boolean) { + if (this.showDialogResolves.has(dialogId)) { + const resolveObj = this.showDialogResolves.get(dialogId); + resolveObj.resolve(confirmed); + this.showDialogResolves.delete(dialogId); } - lockTimeout(): number { - return null; + // Clean up old promises + this.showDialogResolves.forEach((val, key) => { + const age = new Date().getTime() - val.date.getTime(); + if (age > DialogPromiseExpiration) { + this.showDialogResolves.delete(key); + } + }); + } + + async resolvePasswordDialogPromise( + dialogId: number, + canceled: boolean, + password: string + ): Promise { + let result = false; + if (this.passwordDialogResolves.has(dialogId)) { + const resolveObj = this.passwordDialogResolves.get(dialogId); + if (await resolveObj.tryResolve(canceled, password)) { + this.passwordDialogResolves.delete(dialogId); + result = true; + } } - launchUri(uri: string, options?: any): void { - BrowserApi.createNewTab(uri, options && options.extensionPage === true); + // Clean up old promises + this.passwordDialogResolves.forEach((val, key) => { + const age = new Date().getTime() - val.date.getTime(); + if (age > DialogPromiseExpiration) { + this.passwordDialogResolves.delete(key); + } + }); + + return result; + } + + async supportsBiometric() { + const platformInfo = await BrowserApi.getPlatformInfo(); + if (platformInfo.os === "android") { + return false; } - saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void { - BrowserApi.downloadFile(win, blobData, blobOptions, fileName); + if (this.isFirefox()) { + return parseInt((await browser.runtime.getBrowserInfo()).version.split(".")[0], 10) >= 87; } - getApplicationVersion(): Promise { - return Promise.resolve(BrowserApi.getApplicationVersion()); + return true; + } + + authenticateBiometric() { + return this.biometricCallback(); + } + + sidebarViewName(): string { + if ((window as any).chrome.sidebarAction && this.isFirefox()) { + return "sidebar"; + } else if (this.isOpera() && typeof opr !== "undefined" && opr.sidebarAction) { + return "sidebar_panel"; } - supportsWebAuthn(win: Window): boolean { - return (typeof (PublicKeyCredential) !== 'undefined'); - } - - supportsDuo(): boolean { - return true; - } - - showToast(type: 'error' | 'success' | 'warning' | 'info', title: string, text: string | string[], - options?: any): void { - this.messagingService.send('showToast', { - text: text, - title: title, - type: type, - options: options, - }); - } - - showDialog(body: string, title?: string, confirmText?: string, cancelText?: string, type?: string, - bodyIsHtml: boolean = false) { - const dialogId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); - this.messagingService.send('showDialog', { - text: bodyIsHtml ? null : body, - html: bodyIsHtml ? body : null, - title: title, - confirmText: confirmText, - cancelText: cancelText, - type: type, - dialogId: dialogId, - }); - return new Promise(resolve => { - this.showDialogResolves.set(dialogId, { resolve: resolve, date: new Date() }); - }); - } - - isDev(): boolean { - return process.env.ENV === 'development'; - } - - isSelfHost(): boolean { - return false; - } - - copyToClipboard(text: string, options?: any): void { - let win = window; - let doc = window.document; - if (options && (options.window || options.win)) { - win = options.window || options.win; - doc = win.document; - } else if (options && options.doc) { - doc = options.doc; - } - const clearing = options ? !!options.clearing : false; - const clearMs: number = options && options.clearMs ? options.clearMs : null; - - if (this.isSafari()) { - SafariApp.sendMessageToApp('copyToClipboard', text).then(() => { - if (!clearing && this.clipboardWriteCallback != null) { - this.clipboardWriteCallback(text, clearMs); - } - }); - } else if (this.isFirefox() && (win as any).navigator.clipboard && (win as any).navigator.clipboard.writeText) { - (win as any).navigator.clipboard.writeText(text).then(() => { - if (!clearing && this.clipboardWriteCallback != null) { - this.clipboardWriteCallback(text, clearMs); - } - }); - } else if ((win as any).clipboardData && (win as any).clipboardData.setData) { - // IE specific code path to prevent textarea being shown while dialog is visible. - (win as any).clipboardData.setData('Text', text); - if (!clearing && this.clipboardWriteCallback != null) { - this.clipboardWriteCallback(text, clearMs); - } - } else if (doc.queryCommandSupported && doc.queryCommandSupported('copy')) { - if (this.isChrome() && text === '') { - text = '\u0000'; - } - - const textarea = doc.createElement('textarea'); - textarea.textContent = text == null || text === '' ? ' ' : text; - // Prevent scrolling to bottom of page in MS Edge. - textarea.style.position = 'fixed'; - doc.body.appendChild(textarea); - textarea.select(); - - try { - // Security exception may be thrown by some browsers. - if (doc.execCommand('copy') && !clearing && this.clipboardWriteCallback != null) { - this.clipboardWriteCallback(text, clearMs); - } - } catch (e) { - // tslint:disable-next-line - console.warn('Copy to clipboard failed.', e); - } finally { - doc.body.removeChild(textarea); - } - } - } - - async readFromClipboard(options?: any): Promise { - let win = window; - let doc = window.document; - if (options && (options.window || options.win)) { - win = options.window || options.win; - doc = win.document; - } else if (options && options.doc) { - doc = options.doc; - } - - if (this.isSafari()) { - return await SafariApp.sendMessageToApp('readFromClipboard'); - } else if (this.isFirefox() && (win as any).navigator.clipboard && (win as any).navigator.clipboard.readText) { - return await (win as any).navigator.clipboard.readText(); - } else if (doc.queryCommandSupported && doc.queryCommandSupported('paste')) { - const textarea = doc.createElement('textarea'); - // Prevent scrolling to bottom of page in MS Edge. - textarea.style.position = 'fixed'; - doc.body.appendChild(textarea); - textarea.focus(); - try { - // Security exception may be thrown by some browsers. - if (doc.execCommand('paste')) { - return textarea.value; - } - } catch (e) { - // tslint:disable-next-line - console.warn('Read from clipboard failed.', e); - } finally { - doc.body.removeChild(textarea); - } - } - return null; - } - - resolveDialogPromise(dialogId: number, confirmed: boolean) { - if (this.showDialogResolves.has(dialogId)) { - const resolveObj = this.showDialogResolves.get(dialogId); - resolveObj.resolve(confirmed); - this.showDialogResolves.delete(dialogId); - } - - // Clean up old promises - this.showDialogResolves.forEach((val, key) => { - const age = new Date().getTime() - val.date.getTime(); - if (age > DialogPromiseExpiration) { - this.showDialogResolves.delete(key); - } - }); - } - - async resolvePasswordDialogPromise(dialogId: number, canceled: boolean, password: string): Promise { - let result = false; - if (this.passwordDialogResolves.has(dialogId)) { - const resolveObj = this.passwordDialogResolves.get(dialogId); - if (await resolveObj.tryResolve(canceled, password)) { - this.passwordDialogResolves.delete(dialogId); - result = true; - } - } - - // Clean up old promises - this.passwordDialogResolves.forEach((val, key) => { - const age = new Date().getTime() - val.date.getTime(); - if (age > DialogPromiseExpiration) { - this.passwordDialogResolves.delete(key); - } - }); - - return result; - } - - async supportsBiometric() { - const platformInfo = await BrowserApi.getPlatformInfo(); - if (platformInfo.os === 'android') { - return false; - } - - if (this.isFirefox()) { - return parseInt((await browser.runtime.getBrowserInfo()).version.split('.')[0], 10) >= 87; - } - - return true; - } - - authenticateBiometric() { - return this.biometricCallback(); - } - - sidebarViewName(): string { - if ((window as any).chrome.sidebarAction && this.isFirefox()) { - return 'sidebar'; - } else if (this.isOpera() && (typeof opr !== 'undefined') && opr.sidebarAction) { - return 'sidebar_panel'; - } - - return null; - } - - supportsSecureStorage(): boolean { - return false; - } - - getDefaultSystemTheme(): Promise { - return Promise.resolve(this.prefersColorSchemeDark.matches ? ThemeType.Dark : ThemeType.Light); - } - - onDefaultSystemThemeChange(callback: ((theme: ThemeType.Light | ThemeType.Dark) => unknown)) { - this.prefersColorSchemeDark.addEventListener('change', ({ matches }) => { - callback(matches ? ThemeType.Dark : ThemeType.Light); - }); - } - - async getEffectiveTheme() { - const theme = await this.stateService.getTheme() as ThemeType; - if (theme == null || theme === ThemeType.System) { - return this.getDefaultSystemTheme(); - } else { - return theme; - } + return null; + } + + supportsSecureStorage(): boolean { + return false; + } + + getDefaultSystemTheme(): Promise { + return Promise.resolve(this.prefersColorSchemeDark.matches ? ThemeType.Dark : ThemeType.Light); + } + + onDefaultSystemThemeChange(callback: (theme: ThemeType.Light | ThemeType.Dark) => unknown) { + this.prefersColorSchemeDark.addEventListener("change", ({ matches }) => { + callback(matches ? ThemeType.Dark : ThemeType.Light); + }); + } + + async getEffectiveTheme() { + const theme = (await this.stateService.getTheme()) as ThemeType; + if (theme == null || theme === ThemeType.System) { + return this.getDefaultSystemTheme(); + } else { + return theme; } + } } diff --git a/src/services/browserStorage.service.ts b/src/services/browserStorage.service.ts index 3d2c003215..c52fb9588f 100644 --- a/src/services/browserStorage.service.ts +++ b/src/services/browserStorage.service.ts @@ -1,51 +1,51 @@ -import { StorageService } from 'jslib-common/abstractions/storage.service'; +import { StorageService } from "jslib-common/abstractions/storage.service"; export default class BrowserStorageService implements StorageService { - private chromeStorageApi: any; + private chromeStorageApi: any; - constructor() { - this.chromeStorageApi = chrome.storage.local; - } + constructor() { + this.chromeStorageApi = chrome.storage.local; + } - async get(key: string): Promise { - return new Promise(resolve => { - this.chromeStorageApi.get(key, (obj: any) => { - if (obj != null && obj[key] != null) { - resolve(obj[key] as T); - return; - } - resolve(null); - }); - }); - } - - async has(key: string): Promise { - return await this.get(key) != null; - } - - async save(key: string, obj: any): Promise { - if (obj == null) { - // Fix safari not liking null in set - return this.remove(key); + async get(key: string): Promise { + return new Promise((resolve) => { + this.chromeStorageApi.get(key, (obj: any) => { + if (obj != null && obj[key] != null) { + resolve(obj[key] as T); + return; } + resolve(null); + }); + }); + } - if (obj instanceof Set) { - obj = Array.from(obj); - } + async has(key: string): Promise { + return (await this.get(key)) != null; + } - const keyedObj = { [key]: obj }; - return new Promise(resolve => { - this.chromeStorageApi.set(keyedObj, () => { - resolve(); - }); - }); + async save(key: string, obj: any): Promise { + if (obj == null) { + // Fix safari not liking null in set + return this.remove(key); } - async remove(key: string): Promise { - return new Promise(resolve => { - this.chromeStorageApi.remove(key, () => { - resolve(); - }); - }); + if (obj instanceof Set) { + obj = Array.from(obj); } + + const keyedObj = { [key]: obj }; + return new Promise((resolve) => { + this.chromeStorageApi.set(keyedObj, () => { + resolve(); + }); + }); + } + + async remove(key: string): Promise { + return new Promise((resolve) => { + this.chromeStorageApi.remove(key, () => { + resolve(); + }); + }); + } } diff --git a/src/services/state.service.ts b/src/services/state.service.ts index d261f7506f..2e2343b3c8 100644 --- a/src/services/state.service.ts +++ b/src/services/state.service.ts @@ -1,52 +1,75 @@ -import { StorageOptions } from 'jslib-common/models/domain/storageOptions'; -import { StateService as BaseStateService } from 'jslib-common/services/state.service'; - -import { Account } from 'src/models/account'; -import { BrowserComponentState } from '../models/browserComponentState'; -import { BrowserGroupingsComponentState } from '../models/browserGroupingsComponentState'; -import { BrowserSendComponentState } from '../models/browserSendComponentState'; -import { StateService as StateServiceAbstraction } from './abstractions/state.service'; +import { StorageOptions } from "jslib-common/models/domain/storageOptions"; +import { StateService as BaseStateService } from "jslib-common/services/state.service"; +import { Account } from "src/models/account"; +import { BrowserComponentState } from "../models/browserComponentState"; +import { BrowserGroupingsComponentState } from "../models/browserGroupingsComponentState"; +import { BrowserSendComponentState } from "../models/browserSendComponentState"; +import { StateService as StateServiceAbstraction } from "./abstractions/state.service"; export class StateService extends BaseStateService implements StateServiceAbstraction { + async getBrowserGroupingComponentState( + options?: StorageOptions + ): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.groupings; + } - async getBrowserGroupingComponentState(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.groupings; - } + async setBrowserGroupingComponentState( + value: BrowserGroupingsComponentState, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.groupings = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } - async setBrowserGroupingComponentState(value: BrowserGroupingsComponentState, options?: StorageOptions): Promise { - const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); - account.groupings = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } + async getBrowserCipherComponentState(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.ciphers; + } - async getBrowserCipherComponentState(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.ciphers; - } + async setBrowserCipherComponentState( + value: BrowserComponentState, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.ciphers = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } - async setBrowserCipherComponentState(value: BrowserComponentState, options?: StorageOptions): Promise { - const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); - account.ciphers = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } + async getBrowserSendComponentState(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.send; + } - async getBrowserSendComponentState(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.send; - } - - async setBrowserSendComponentState(value: BrowserSendComponentState, options?: StorageOptions): Promise { - const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); - account.send = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - async getBrowserSendTypeComponentState(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.sendType; - } - - async setBrowserSendTypeComponentState(value: BrowserComponentState, options?: StorageOptions): Promise { - const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); - account.sendType = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } + async setBrowserSendComponentState( + value: BrowserSendComponentState, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.send = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + async getBrowserSendTypeComponentState(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.sendType; + } + async setBrowserSendTypeComponentState( + value: BrowserComponentState, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.sendType = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } }