Run prettier

This commit is contained in:
Robyn MacCallum 2021-12-21 14:48:22 -05:00
parent 40cbf61ab7
commit 42bb8cd5f8
40 changed files with 5928 additions and 5299 deletions

View File

@ -1,66 +1,71 @@
import { NotificationsService } from 'jslib-common/abstractions/notifications.service'; import { NotificationsService } from "jslib-common/abstractions/notifications.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { StateService } from '../services/abstractions/state.service'; import { StateService } from "../services/abstractions/state.service";
const IdleInterval = 60 * 5; // 5 minutes const IdleInterval = 60 * 5; // 5 minutes
export default class IdleBackground { export default class IdleBackground {
private idle: any; private idle: any;
private idleTimer: number = null; private idleTimer: number = null;
private idleState = 'active'; private idleState = "active";
constructor(private vaultTimeoutService: VaultTimeoutService, private stateService: StateService, constructor(
private notificationsService: NotificationsService) { private vaultTimeoutService: VaultTimeoutService,
this.idle = chrome.idle || (browser != null ? browser.idle : null); private stateService: StateService,
private notificationsService: NotificationsService
) {
this.idle = chrome.idle || (browser != null ? browser.idle : null);
}
async init() {
if (!this.idle) {
return;
} }
async init() { const idleHandler = (newState: string) => {
if (!this.idle) { if (newState === "active") {
return; 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 (this.idle.onStateChanged) {
if (newState === 'active') { this.idle.onStateChanged.addListener(async (newState: string) => {
this.notificationsService.reconnectFromActivity(); 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 { } 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) { private pollIdle(handler: (newState: string) => void) {
if (this.idleTimer != null) { if (this.idleTimer != null) {
window.clearTimeout(this.idleTimer); window.clearTimeout(this.idleTimer);
this.idleTimer = null; 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);
});
} }
this.idle.queryState(IdleInterval, (state: string) => {
if (state !== this.idleState) {
this.idleState = state;
handler(state);
}
this.idleTimer = window.setTimeout(() => this.pollIdle(handler), 5000);
});
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,331 +1,346 @@
import { AppIdService } from 'jslib-common/abstractions/appId.service'; import { AppIdService } from "jslib-common/abstractions/appId.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey'; import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { BrowserApi } from '../browser/browserApi'; import { BrowserApi } from "../browser/browserApi";
import RuntimeBackground from './runtime.background'; import RuntimeBackground from "./runtime.background";
const MessageValidTimeout = 10 * 1000; const MessageValidTimeout = 10 * 1000;
const EncryptionAlgorithm = 'sha1'; const EncryptionAlgorithm = "sha1";
export class NativeMessagingBackground { export class NativeMessagingBackground {
private connected = false; private connected = false;
private connecting: boolean; private connecting: boolean;
private port: browser.runtime.Port | chrome.runtime.Port; private port: browser.runtime.Port | chrome.runtime.Port;
private resolver: any = null; private resolver: any = null;
private privateKey: ArrayBuffer = null; private privateKey: ArrayBuffer = null;
private publicKey: ArrayBuffer = null; private publicKey: ArrayBuffer = null;
private secureSetupResolve: any = null; private secureSetupResolve: any = null;
private sharedSecret: SymmetricCryptoKey; private sharedSecret: SymmetricCryptoKey;
private appId: string; private appId: string;
private validatingFingerprint: boolean; private validatingFingerprint: boolean;
constructor(private cryptoService: CryptoService, constructor(
private cryptoFunctionService: CryptoFunctionService, private cryptoService: CryptoService,
private runtimeBackground: RuntimeBackground, private i18nService: I18nService, private cryptoFunctionService: CryptoFunctionService,
private messagingService: MessagingService, private appIdService: AppIdService, private runtimeBackground: RuntimeBackground,
private platformUtilsService: PlatformUtilsService, private stateService: StateService) { private i18nService: I18nService,
this.stateService.setBiometricFingerprintValidated(false); private messagingService: MessagingService,
private appIdService: AppIdService,
private platformUtilsService: PlatformUtilsService,
private stateService: StateService
) {
this.stateService.setBiometricFingerprintValidated(false);
if (chrome?.permissions?.onAdded) { if (chrome?.permissions?.onAdded) {
// Reload extension to activate nativeMessaging // Reload extension to activate nativeMessaging
chrome.permissions.onAdded.addListener(permissions => { chrome.permissions.onAdded.addListener((permissions) => {
BrowserApi.reloadExtension(null); BrowserApi.reloadExtension(null);
}); });
}
} }
}
async connect() { async connect() {
this.appId = await this.appIdService.getAppId(); this.appId = await this.appIdService.getAppId();
this.stateService.setBiometricFingerprintValidated(false); this.stateService.setBiometricFingerprintValidated(false);
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
this.port = BrowserApi.connectNative('com.8bit.bitwarden'); this.port = BrowserApi.connectNative("com.8bit.bitwarden");
this.connecting = true; this.connecting = true;
const connectedCallback = () => { const connectedCallback = () => {
this.connected = true; this.connected = true;
this.connecting = false; this.connecting = false;
resolve(); resolve();
}; };
// Safari has a bundled native component which is always available, no need to // Safari has a bundled native component which is always available, no need to
// check if the desktop app is running. // check if the desktop app is running.
if (this.platformUtilsService.isSafari()) { if (this.platformUtilsService.isSafari()) {
connectedCallback(); 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) => { const encrypted = Utils.fromB64ToArray(message.sharedSecret);
switch (message.command) { const decrypted = await this.cryptoFunctionService.rsaDecrypt(
case 'connected': encrypted.buffer,
connectedCallback(); this.privateKey,
break; EncryptionAlgorithm
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); if (this.validatingFingerprint) {
const decrypted = await this.cryptoFunctionService.rsaDecrypt(encrypted.buffer, this.privateKey, EncryptionAlgorithm); this.validatingFingerprint = false;
this.stateService.setBiometricFingerprintValidated(true);
if (this.validatingFingerprint) { }
this.validatingFingerprint = false; this.sharedSecret = new SymmetricCryptoKey(decrypted);
this.stateService.setBiometricFingerprintValidated(true); this.secureSetupResolve();
} break;
this.sharedSecret = new SymmetricCryptoKey(decrypted); case "invalidateEncryption":
this.secureSetupResolve(); // Ignore since it belongs to another device
break; if (message.appId !== this.appId) {
case 'invalidateEncryption': return;
// 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<any> {
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.sharedSecret = null;
this.privateKey = null; this.privateKey = null;
this.connected = false; this.connected = false;
this.messagingService.send('showDialog', { this.messagingService.send("showDialog", {
text: this.i18nService.t('nativeMessagingInvalidEncryptionDesc'), text: this.i18nService.t("nativeMessagingInvalidEncryptionDesc"),
title: this.i18nService.t('nativeMessagingInvalidEncryptionTitle'), title: this.i18nService.t("nativeMessagingInvalidEncryptionTitle"),
confirmText: this.i18nService.t('ok'), confirmText: this.i18nService.t("ok"),
type: 'error', 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) { if (this.platformUtilsService.isSafari()) {
let message = rawMessage; this.postMessage(message);
if (!this.platformUtilsService.isSafari()) { } else {
message = JSON.parse(await this.cryptoService.decryptToUtf8(rawMessage, this.sharedSecret)); 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<any> {
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 // tslint:disable-next-line
console.error('NativeMessage is to old, ignoring.'); console.error("Unable to verify key:", e);
return; await this.cryptoService.clearKey();
} this.showWrongUserDialog();
switch (message.command) { message = false;
case 'biometricUnlock': break;
await this.stateService.setBiometricAwaitingAcceptance(null); }
if (message.response === 'not enabled') { await this.stateService.setBiometricLocked(false);
this.messagingService.send('showDialog', { this.runtimeBackground.processMessage({ command: "unlocked" }, null, null);
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);
} }
break;
default:
// tslint:disable-next-line
console.error("NativeMessage, got unknown command: ", message.command);
} }
private async secureCommunication() { if (this.resolver) {
const [publicKey, privateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); this.resolver(message);
this.publicKey = publicKey; }
this.privateKey = privateKey; }
this.sendUnencrypted({ private async secureCommunication() {
command: 'setupEncryption', const [publicKey, privateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
publicKey: Utils.fromBufferToB64(publicKey), this.publicKey = publicKey;
userId: await this.stateService.getUserId(), 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) { message.timestamp = Date.now();
if (!this.connected) {
await this.connect();
}
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() { this.messagingService.send("showDialog", {
const fingerprint = (await this.cryptoService.getFingerprint(await this.stateService.getUserId(), this.publicKey)).join(' '); html: `${this.i18nService.t(
"desktopIntegrationVerificationText"
this.messagingService.send('showDialog', { )}<br><br><strong>${fingerprint}</strong>`,
html: `${this.i18nService.t('desktopIntegrationVerificationText')}<br><br><strong>${fingerprint}</strong>`, title: this.i18nService.t("desktopSyncVerificationTitle"),
title: this.i18nService.t('desktopSyncVerificationTitle'), confirmText: this.i18nService.t("ok"),
confirmText: this.i18nService.t('ok'), type: "warning",
type: 'warning', });
}); }
}
} }

View File

@ -28,505 +28,423 @@ import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNoti
import { NotificationQueueMessageType } from "./models/notificationQueueMessageType"; import { NotificationQueueMessageType } from "./models/notificationQueueMessageType";
export default class NotificationBackground { export default class NotificationBackground {
private notificationQueue: ( private notificationQueue: (AddLoginQueueMessage | AddChangePasswordQueueMessage)[] = [];
| AddLoginQueueMessage
| AddChangePasswordQueueMessage
)[] = [];
constructor( constructor(
private main: MainBackground, private main: MainBackground,
private autofillService: AutofillService, private autofillService: AutofillService,
private cipherService: CipherService, private cipherService: CipherService,
private storageService: StorageService, private storageService: StorageService,
private vaultTimeoutService: VaultTimeoutService, private vaultTimeoutService: VaultTimeoutService,
private policyService: PolicyService, private policyService: PolicyService,
private folderService: FolderService, private folderService: FolderService,
private stateService: StateService private stateService: StateService
) {} ) {}
async init() { async init() {
if (chrome.runtime == null) { if (chrome.runtime == null) {
return; return;
}
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) { BrowserApi.messageListener(
switch (msg.command) { "notification.background",
case "unlockCompleted": async (msg: any, sender: chrome.runtime.MessageSender) => {
if (msg.data.target !== "notification.background") { await this.processMessage(msg, sender);
return; }
} );
await this.processMessage(
msg.data.commandToRetry.msg, this.cleanupNotificationQueue();
msg.data.commandToRetry.sender }
);
break; async processMessage(msg: any, sender: chrome.runtime.MessageSender) {
case "bgGetDataForTab": switch (msg.command) {
await this.getDataForTab(sender.tab, msg.responseCommand); case "unlockCompleted":
break; if (msg.data.target !== "notification.background") {
case "bgCloseNotificationBar": return;
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;
} }
} await this.processMessage(msg.data.commandToRetry.msg, msg.data.commandToRetry.sender);
break;
async checkNotificationQueue(tab: chrome.tabs.Tab = null): Promise<void> { case "bgGetDataForTab":
if (this.notificationQueue.length === 0) { await this.getDataForTab(sender.tab, msg.responseCommand);
return; 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);
if (tab != null) { break;
this.doNotificationQueueCheck(tab); case "bgNeverSave":
return; await this.saveNever(sender.tab);
} break;
case "collectPageDetailsResponse":
const currentTab = await BrowserApi.getTabFromCurrentWindow(); switch (msg.sender) {
if (currentTab != null) { case "notificationBar":
this.doNotificationQueueCheck(currentTab); const forms = this.autofillService.getFormsWithPasswordFields(msg.details);
} await BrowserApi.tabSendMessageData(msg.tab, "notificationBarPageDetails", {
} details: msg.details,
forms: forms,
private cleanupNotificationQueue() { });
for (let i = this.notificationQueue.length - 1; i >= 0; i--) { break;
if (this.notificationQueue[i].expires < new Date()) { default:
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; break;
} }
break;
default:
break;
}
}
async checkNotificationQueue(tab: chrome.tabs.Tab = null): Promise<void> {
if (this.notificationQueue.length === 0) {
return;
} }
private removeTabFromNotificationQueue(tab: chrome.tabs.Tab) { if (tab != null) {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) { this.doNotificationQueueCheck(tab);
if (this.notificationQueue[i].tabId === tab.id) { return;
this.notificationQueue.splice(i, 1);
}
}
} }
private async addLogin( const currentTab = await BrowserApi.getTabFromCurrentWindow();
loginInfo: AddLoginRuntimeMessage, if (currentTab != null) {
tab: chrome.tabs.Tab 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())) { const disabledChangePassword =
return; 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 (!queueMessage.wasVaultLocked) {
if (loginDomain == null) { await this.createNewCipher(queueMessage as AddLoginQueueMessage, folderId);
return; }
}
let normalizedUsername = loginInfo.username; // If the vault was locked, check if a cipher needs updating instead of creating a new one
if (normalizedUsername != null) { if (
normalizedUsername = normalizedUsername.toLowerCase(); queueMessage.type === NotificationQueueMessageType.addLogin &&
} queueMessage.wasVaultLocked === true
) {
if (await this.vaultTimeoutService.isLocked()) { const message = queueMessage as AddLoginQueueMessage;
if (!(await this.allowPersonalOwnership())) { const ciphers = await this.cipherService.getAllDecryptedForUrl(message.uri);
return;
}
this.pushAddLoginToQueue(loginDomain, loginInfo, tab, true);
return;
}
const ciphers = await this.cipherService.getAllDecryptedForUrl(
loginInfo.url
);
const usernameMatches = ciphers.filter( const usernameMatches = ciphers.filter(
(c) => (c) => c.login.username != null && c.login.username.toLowerCase() === message.username
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())) { if (usernameMatches.length >= 1) {
return; await this.updateCipher(usernameMatches[0], message.password);
} 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 (await this.vaultTimeoutService.isLocked()) { await this.createNewCipher(message, folderId);
this.pushChangePasswordToQueue( }
null, }
loginDomain, }
changeData.newPassword,
tab,
true
);
return;
}
let id: string = null; private async createNewCipher(queueMessage: AddLoginQueueMessage, folderId: string) {
const ciphers = await this.cipherService.getAllDecryptedForUrl( const loginModel = new LoginView();
changeData.url const loginUri = new LoginUriView();
); loginUri.uri = queueMessage.uri;
if (changeData.currentPassword != null) { loginModel.uris = [loginUri];
const passwordMatches = ciphers.filter( loginModel.username = queueMessage.username;
(c) => c.login.password === changeData.currentPassword loginModel.password = queueMessage.password;
); const model = new CipherView();
if (passwordMatches.length === 1) { model.name = Utils.getHostname(queueMessage.uri) || queueMessage.domain;
id = passwordMatches[0].id; model.name = model.name.replace(/^www\./, "");
} model.type = CipherType.Login;
} else if (ciphers.length === 1) { model.login = loginModel;
id = ciphers[0].id;
} if (!Utils.isNullOrWhitespace(folderId)) {
if (id != null) { const folders = await this.folderService.getAllDecrypted();
this.pushChangePasswordToQueue( if (folders.some((x) => x.id === folderId)) {
id, model.folderId = folderId;
loginDomain, }
changeData.newPassword,
tab
);
}
} }
private async pushChangePasswordToQueue( const cipher = await this.cipherService.encrypt(model);
cipherId: string, await this.cipherService.saveWithServer(cipher);
loginDomain: string, }
newPassword: string,
tab: chrome.tabs.Tab, private async getDecryptedCipherById(cipherId: string) {
isVaultLocked: boolean = false const cipher = await this.cipherService.get(cipherId);
) { if (cipher != null && cipher.type === CipherType.Login) {
// remove any old messages for this tab return await cipher.decrypt();
this.removeTabFromNotificationQueue(tab); }
const message: AddChangePasswordQueueMessage = { return null;
type: NotificationQueueMessageType.changePassword, }
cipherId: cipherId,
newPassword: newPassword, private async updateCipher(cipher: CipherView, newPassword: string) {
domain: loginDomain, if (cipher != null && cipher.type === CipherType.Login) {
tabId: tab.id, cipher.login.password = newPassword;
expires: new Date(new Date().getTime() + 5 * 60000), // 5 minutes const newCipher = await this.cipherService.encrypt(cipher);
wasVaultLocked: isVaultLocked, await this.cipherService.saveWithServer(newCipher);
}; }
this.notificationQueue.push(message); }
await this.checkNotificationQueue(tab);
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( await BrowserApi.tabSendMessageData(tab, responseCommand, responseData);
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); private async allowPersonalOwnership(): Promise<boolean> {
if (tabDomain != null && tabDomain !== queueMessage.domain) { return !(await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership));
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<boolean> {
return !(await this.policyService.policyAppliesToUser(
PolicyType.PersonalOwnership
));
}
} }

View File

@ -1,220 +1,245 @@
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { NotificationsService } from 'jslib-common/abstractions/notifications.service'; import { NotificationsService } from "jslib-common/abstractions/notifications.service";
import { SystemService } from 'jslib-common/abstractions/system.service'; import { SystemService } from "jslib-common/abstractions/system.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { AutofillService } from '../services/abstractions/autofill.service'; import { AutofillService } from "../services/abstractions/autofill.service";
import BrowserPlatformUtilsService from '../services/browserPlatformUtils.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 { StateService } from "jslib-common/abstractions/state.service";
import LockedVaultPendingNotificationsItem from './models/lockedVaultPendingNotificationsItem'; import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNotificationsItem";
export default class RuntimeBackground { export default class RuntimeBackground {
private autofillTimeout: any; private autofillTimeout: any;
private pageDetailsToAutoFill: any[] = []; private pageDetailsToAutoFill: any[] = [];
private onInstalledReason: string = null; private onInstalledReason: string = null;
private lockedVaultPendingNotifications: LockedVaultPendingNotificationsItem[] = []; private lockedVaultPendingNotifications: LockedVaultPendingNotificationsItem[] = [];
constructor(private main: MainBackground, private autofillService: AutofillService, constructor(
private platformUtilsService: BrowserPlatformUtilsService, private main: MainBackground,
private i18nService: I18nService, private autofillService: AutofillService,
private notificationsService: NotificationsService, private systemService: SystemService, private platformUtilsService: BrowserPlatformUtilsService,
private environmentService: EnvironmentService, private messagingService: MessagingService, private i18nService: I18nService,
private stateService: StateService, private logService: LogService) { 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 async init() {
chrome.runtime.onInstalled.addListener((details: any) => { if (!chrome.runtime) {
this.onInstalledReason = details.reason; return;
});
} }
async init() { await this.checkOnInstalled();
if (!chrome.runtime) { BrowserApi.messageListener(
return; "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(); await this.main.setIcon();
BrowserApi.messageListener('runtime.background', async (msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) => { await this.main.refreshBadgeAndMenu(false);
await this.processMessage(msg, sender, sendResponse); this.notificationsService.updateConnection(msg.command === "unlocked");
}); this.systemService.cancelProcessReload();
}
async processMessage(msg: any, sender: any, sendResponse: any) { if (item) {
switch (msg.command) { await BrowserApi.tabSendMessageData(
case 'loggedIn': item.commandToRetry.sender.tab,
case 'unlocked': "unlockCompleted",
let item: LockedVaultPendingNotificationsItem; item
);
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;
} }
} break;
case "addToLockedVaultPendingNotifications":
private async autofillPage() { this.lockedVaultPendingNotifications.push(msg.data);
const totpCode = await this.autofillService.doAutoFill({ break;
cipher: this.main.loginToAutoFill, case "logout":
pageDetails: this.pageDetailsToAutoFill, await this.main.logout(msg.expired);
fillNewPassword: true, break;
}); case "syncCompleted":
if (msg.successfully) {
if (totpCode != null) { setTimeout(async () => await this.main.refreshBadgeAndMenu(), 2000);
this.platformUtilsService.copyToClipboard(totpCode, { window: window });
} }
break;
// reset case "openPopup":
this.main.loginToAutoFill = null; await this.main.openPopup();
this.pageDetailsToAutoFill = []; break;
} case "promptForLogin":
await BrowserApi.createNewTab("popup/index.html?uilocation=popout", true, true);
private async checkOnInstalled() { break;
setTimeout(async () => { case "showDialogResolve":
if (this.onInstalledReason != null) { this.platformUtilsService.resolveDialogPromise(msg.dialogId, msg.confirmed);
if (this.onInstalledReason === 'install') { break;
BrowserApi.createNewTab('https://bitwarden.com/browser-start/'); case "bgCollectPageDetails":
await this.setDefaultSettings(); await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId);
} break;
case "bgUpdateContextMenu":
this.onInstalledReason = null; 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() { if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) {
// Default timeout option to "on restart". return;
const currentVaultTimeout = await this.stateService.getVaultTimeout();
if (currentVaultTimeout == null) {
await this.stateService.setVaultTimeout(-1);
} }
// Default action to "lock". try {
const currentVaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); BrowserApi.createNewTab(
if (currentVaultTimeoutAction == null) { "popup/index.html?uilocation=popout#/sso?code=" +
await this.stateService.setVaultTimeoutAction('lock'); 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");
}
}
} }

View File

@ -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 { BrowserComponentState } from "./browserComponentState";
import { BrowserGroupingsComponentState } from './browserGroupingsComponentState'; import { BrowserGroupingsComponentState } from "./browserGroupingsComponentState";
import { BrowserSendComponentState } from './browserSendComponentState'; import { BrowserSendComponentState } from "./browserSendComponentState";
export class Account extends BaseAccount { export class Account extends BaseAccount {
groupings?: BrowserGroupingsComponentState; groupings?: BrowserGroupingsComponentState;
send?: BrowserSendComponentState; send?: BrowserSendComponentState;
ciphers?: BrowserComponentState; ciphers?: BrowserComponentState;
sendType?: BrowserComponentState; sendType?: BrowserComponentState;
constructor(init: Partial<Account>) { constructor(init: Partial<Account>) {
super(init); super(init);
this.groupings = init.groupings ?? this.groupings = init.groupings ?? new BrowserGroupingsComponentState();
new BrowserGroupingsComponentState(); this.send = init.send ?? new BrowserSendComponentState();
this.send = init.send ?? this.ciphers = init.ciphers ?? new BrowserComponentState();
new BrowserSendComponentState(); this.sendType = init.sendType ?? new BrowserComponentState();
this.ciphers = init.ciphers ?? }
new BrowserComponentState();
this.sendType = init.sendType ??
new BrowserComponentState();
}
} }

View File

@ -1,4 +1,4 @@
export class BrowserComponentState { export class BrowserComponentState {
scrollY: number; scrollY: number;
searchText: string; searchText: string;
} }

View File

@ -1,15 +1,15 @@
import { CipherType } from 'jslib-common/enums/cipherType'; import { CipherType } from "jslib-common/enums/cipherType";
import { CipherView } from 'jslib-common/models/view/cipherView'; import { CipherView } from "jslib-common/models/view/cipherView";
import { CollectionView } from 'jslib-common/models/view/collectionView'; import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from 'jslib-common/models/view/folderView'; import { FolderView } from "jslib-common/models/view/folderView";
import { BrowserComponentState } from './browserComponentState'; import { BrowserComponentState } from "./browserComponentState";
export class BrowserGroupingsComponentState extends BrowserComponentState { export class BrowserGroupingsComponentState extends BrowserComponentState {
favoriteCiphers: CipherView[]; favoriteCiphers: CipherView[];
noFolderCiphers: CipherView[]; noFolderCiphers: CipherView[];
collectionCounts: Map<string, number>; collectionCounts: Map<string, number>;
typeCounts: Map<CipherType, number>; typeCounts: Map<CipherType, number>;
folders: FolderView[]; folders: FolderView[];
collections: CollectionView[]; collections: CollectionView[];
deletedCount: number; deletedCount: number;
} }

View File

@ -1,8 +1,8 @@
import { SendType } from 'jslib-common/enums/sendType'; import { SendType } from "jslib-common/enums/sendType";
import { SendView } from 'jslib-common/models/view/sendView'; import { SendView } from "jslib-common/models/view/sendView";
import { BrowserComponentState } from './browserComponentState'; import { BrowserComponentState } from "./browserComponentState";
export class BrowserSendComponentState extends BrowserComponentState { export class BrowserSendComponentState extends BrowserComponentState {
sends: SendView[]; sends: SendView[];
typeCounts: Map<SendType, number>; typeCounts: Map<SendType, number>;
} }

View File

@ -1,51 +1,64 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
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";
@Component({ @Component({
selector: 'app-home', selector: "app-home",
templateUrl: 'home.component.html', templateUrl: "home.component.html",
}) })
export class HomeComponent { export class HomeComponent {
constructor(protected platformUtilsService: PlatformUtilsService, constructor(
private passwordGenerationService: PasswordGenerationService, private stateService: StateService, protected platformUtilsService: PlatformUtilsService,
private cryptoFunctionService: CryptoFunctionService, private environmentService: EnvironmentService) { } private passwordGenerationService: PasswordGenerationService,
private stateService: StateService,
private cryptoFunctionService: CryptoFunctionService,
private environmentService: EnvironmentService
) {}
async launchSsoBrowser() { async launchSsoBrowser() {
// Generate necessary sso params // Generate necessary sso params
const passwordOptions: any = { const passwordOptions: any = {
type: 'password', type: "password",
length: 64, length: 64,
uppercase: true, uppercase: true,
lowercase: true, lowercase: true,
numbers: true, numbers: true,
special: false, special: false,
}; };
const state = (await this.passwordGenerationService.generatePassword(passwordOptions)) + ':clientId=browser'; const state =
const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); (await this.passwordGenerationService.generatePassword(passwordOptions)) +
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, 'sha256'); ":clientId=browser";
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); 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.setSsoCodeVerifier(codeVerifier);
await this.stateService.setSsoState(state); await this.stateService.setSsoState(state);
let url = this.environmentService.getWebVaultUrl(); let url = this.environmentService.getWebVaultUrl();
if (url == null) { if (url == null) {
url = 'https://vault.bitwarden.com'; 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);
} }
const redirectUri = url + "/sso-connector.html";
// Launch browser
this.platformUtilsService.launchUri(
url +
"/#/sso?clientId=browser" +
"&redirectUri=" +
encodeURIComponent(redirectUri) +
"&state=" +
state +
"&codeChallenge=" +
codeChallenge
);
}
} }

View File

@ -1,77 +1,99 @@
import { Component, NgZone } from '@angular/core'; import { Component, NgZone } from "@angular/core";
import { Router } from '@angular/router'; import { Router } from "@angular/router";
import Swal from 'sweetalert2'; import Swal from "sweetalert2";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.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 { LockComponent as BaseLockComponent } from "jslib-angular/components/lock.component";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
@Component({ @Component({
selector: 'app-lock', selector: "app-lock",
templateUrl: 'lock.component.html', templateUrl: "lock.component.html",
}) })
export class LockComponent extends BaseLockComponent { export class LockComponent extends BaseLockComponent {
private isInitialLockScreen: boolean; private isInitialLockScreen: boolean;
constructor(router: Router, i18nService: I18nService, constructor(
platformUtilsService: PlatformUtilsService, messagingService: MessagingService, cryptoService: CryptoService, router: Router,
vaultTimeoutService: VaultTimeoutService, environmentService: EnvironmentService, i18nService: I18nService,
stateService: StateService, apiService: ApiService, logService: LogService, keyConnectorService: KeyConnectorService, ngZone: NgZone) { platformUtilsService: PlatformUtilsService,
super(router, i18nService, platformUtilsService, messagingService: MessagingService,
messagingService, cryptoService, vaultTimeoutService, cryptoService: CryptoService,
environmentService, stateService, apiService, logService, keyConnectorService, ngZone); vaultTimeoutService: VaultTimeoutService,
this.successRoute = '/tabs/current'; environmentService: EnvironmentService,
this.isInitialLockScreen = (window as any).previousPopupUrl == null; 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() { async ngOnInit() {
await super.ngOnInit(); await super.ngOnInit();
const disableAutoBiometricsPrompt = await this.stateService.getDisableAutoBiometricsPrompt() ?? true; const disableAutoBiometricsPrompt =
(await this.stateService.getDisableAutoBiometricsPrompt()) ?? true;
window.setTimeout(async () => { window.setTimeout(async () => {
document.getElementById(this.pinLock ? 'pin' : 'masterPassword').focus(); document.getElementById(this.pinLock ? "pin" : "masterPassword").focus();
if (this.biometricLock && !disableAutoBiometricsPrompt && this.isInitialLockScreen) { if (this.biometricLock && !disableAutoBiometricsPrompt && this.isInitialLockScreen) {
if (await this.vaultTimeoutService.isLocked()) { if (await this.vaultTimeoutService.isLocked()) {
await this.unlockBiometric(); await this.unlockBiometric();
}
}
}, 100);
}
async unlockBiometric(): Promise<boolean> {
if (!this.biometricLock) {
return;
} }
}
}, 100);
}
const div = document.createElement('div'); async unlockBiometric(): Promise<boolean> {
div.innerHTML = `<div class="swal2-text">${this.i18nService.t('awaitDesktop')}</div>`; if (!this.biometricLock) {
return;
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;
} }
const div = document.createElement("div");
div.innerHTML = `<div class="swal2-text">${this.i18nService.t("awaitDesktop")}</div>`;
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;
}
} }

View File

@ -1,45 +1,61 @@
import { Component, NgZone } from '@angular/core'; import { Component, NgZone } from "@angular/core";
import { Router } from '@angular/router'; import { Router } from "@angular/router";
import { AuthService } from 'jslib-common/abstractions/auth.service'; import { AuthService } from "jslib-common/abstractions/auth.service";
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { StorageService } from 'jslib-common/abstractions/storage.service'; import { StorageService } from "jslib-common/abstractions/storage.service";
import { SyncService } from 'jslib-common/abstractions/sync.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({ @Component({
selector: 'app-login', selector: "app-login",
templateUrl: 'login.component.html', templateUrl: "login.component.html",
}) })
export class LoginComponent extends BaseLoginComponent { export class LoginComponent extends BaseLoginComponent {
constructor(authService: AuthService, router: Router, constructor(
protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, authService: AuthService,
protected stateService: StateService, protected environmentService: EnvironmentService, router: Router,
protected passwordGenerationService: PasswordGenerationService, protected platformUtilsService: PlatformUtilsService,
protected cryptoFunctionService: CryptoFunctionService, logService: LogService, protected i18nService: I18nService,
syncService: SyncService, ngZone: NgZone) { protected stateService: StateService,
super(authService, router, platformUtilsService, i18nService, protected environmentService: EnvironmentService,
stateService, environmentService, passwordGenerationService, cryptoFunctionService, protected passwordGenerationService: PasswordGenerationService,
logService, ngZone); protected cryptoFunctionService: CryptoFunctionService,
super.onSuccessfulLogin = async () => { logService: LogService,
await syncService.fullSync(true); syncService: SyncService,
}; ngZone: NgZone
super.successRoute = '/tabs/vault'; ) {
} super(
authService,
router,
platformUtilsService,
i18nService,
stateService,
environmentService,
passwordGenerationService,
cryptoFunctionService,
logService,
ngZone
);
super.onSuccessfulLogin = async () => {
await syncService.fullSync(true);
};
super.successRoute = "/tabs/vault";
}
settings() { settings() {
this.router.navigate(['environment']); this.router.navigate(["environment"]);
} }
async submit() { async submit() {
await super.submit(); await super.submit();
this.stateService.setRememberedEmail(this.email); this.stateService.setRememberedEmail(this.email);
} }
} }

View File

@ -1,65 +1,79 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { import { ActivatedRoute, Router } from "@angular/router";
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from 'jslib-common/abstractions/sync.service'; import { SyncService } from "jslib-common/abstractions/sync.service";
import { import { SetPasswordComponent as BaseSetPasswordComponent } from "jslib-angular/components/set-password.component";
SetPasswordComponent as BaseSetPasswordComponent,
} from 'jslib-angular/components/set-password.component';
@Component({ @Component({
selector: 'app-set-password', selector: "app-set-password",
templateUrl: 'set-password.component.html', templateUrl: "set-password.component.html",
}) })
export class SetPasswordComponent extends BaseSetPasswordComponent { export class SetPasswordComponent extends BaseSetPasswordComponent {
constructor(apiService: ApiService, i18nService: I18nService, constructor(
cryptoService: CryptoService, messagingService: MessagingService, apiService: ApiService,
stateService: StateService, passwordGenerationService: PasswordGenerationService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, policyService: PolicyService, router: Router, cryptoService: CryptoService,
syncService: SyncService, route: ActivatedRoute) { messagingService: MessagingService,
super(i18nService, cryptoService, messagingService, passwordGenerationService, stateService: StateService,
platformUtilsService, policyService, router, apiService, syncService, route, 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() { get masterPasswordScoreWidth() {
return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20; return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
} }
get masterPasswordScoreColor() { get masterPasswordScoreColor() {
switch (this.masterPasswordScore) { switch (this.masterPasswordScore) {
case 4: case 4:
return 'success'; return "success";
case 3: case 3:
return 'primary'; return "primary";
case 2: case 2:
return 'warning'; return "warning";
default: default:
return 'danger'; return "danger";
}
} }
}
get masterPasswordScoreText() { get masterPasswordScoreText() {
switch (this.masterPasswordScore) { switch (this.masterPasswordScore) {
case 4: case 4:
return this.i18nService.t('strong'); return this.i18nService.t("strong");
case 3: case 3:
return this.i18nService.t('good'); return this.i18nService.t("good");
case 2: case 2:
return this.i18nService.t('weak'); return this.i18nService.t("weak");
default: default:
return this.masterPasswordScore != null ? this.i18nService.t('weak') : null; return this.masterPasswordScore != null ? this.i18nService.t("weak") : null;
}
} }
}
} }

View File

@ -1,54 +1,71 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { import { ActivatedRoute, Router } from "@angular/router";
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from 'jslib-common/abstractions/auth.service'; import { AuthService } from "jslib-common/abstractions/auth.service";
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SyncService } from 'jslib-common/abstractions/sync.service'; import { SyncService } from "jslib-common/abstractions/sync.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { SsoComponent as BaseSsoComponent } from 'jslib-angular/components/sso.component'; import { SsoComponent as BaseSsoComponent } from "jslib-angular/components/sso.component";
import { BrowserApi } from '../../browser/browserApi'; import { BrowserApi } from "../../browser/browserApi";
import { StateService } from '../../services/abstractions/state.service'; import { StateService } from "../../services/abstractions/state.service";
@Component({ @Component({
selector: 'app-sso', selector: "app-sso",
templateUrl: 'sso.component.html', templateUrl: "sso.component.html",
}) })
export class SsoComponent extends BaseSsoComponent { export class SsoComponent extends BaseSsoComponent {
constructor(authService: AuthService, router: Router, constructor(
i18nService: I18nService, route: ActivatedRoute, stateService: StateService, authService: AuthService,
platformUtilsService: PlatformUtilsService, apiService: ApiService, router: Router,
cryptoFunctionService: CryptoFunctionService, passwordGenerationService: PasswordGenerationService, i18nService: I18nService,
syncService: SyncService, environmentService: EnvironmentService, logService: LogService, route: ActivatedRoute,
private vaultTimeoutService: VaultTimeoutService) { stateService: StateService,
super(authService, router, i18nService, route, stateService, platformUtilsService, platformUtilsService: PlatformUtilsService,
apiService, cryptoFunctionService, environmentService, passwordGenerationService, logService); 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.redirectUri = url + "/sso-connector.html";
this.clientId = 'browser'; this.clientId = "browser";
super.onSuccessfulLogin = async () => { super.onSuccessfulLogin = async () => {
await syncService.fullSync(true); await syncService.fullSync(true);
if (await this.vaultTimeoutService.isLocked()) { if (await this.vaultTimeoutService.isLocked()) {
// If the vault is unlocked then this will clear keys from memory, which we don't want to do // If the vault is unlocked then this will clear keys from memory, which we don't want to do
BrowserApi.reloadOpenWindows(); BrowserApi.reloadOpenWindows();
} }
const thisWindow = window.open('', '_self'); const thisWindow = window.open("", "_self");
thisWindow.close(); thisWindow.close();
}; };
} }
} }

View File

@ -1,115 +1,139 @@
import { ChangeDetectorRef, Component, NgZone } from '@angular/core'; import { ChangeDetectorRef, Component, NgZone } from "@angular/core";
import { import { ActivatedRoute, Router } from "@angular/router";
ActivatedRoute, import { first } from "rxjs/operators";
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 { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from 'jslib-common/abstractions/auth.service'; import { AuthService } from "jslib-common/abstractions/auth.service";
import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SyncService } from 'jslib-common/abstractions/sync.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 { StateService } from "jslib-common/abstractions/state.service";
import { BrowserApi } from '../../browser/browserApi'; import { BrowserApi } from "../../browser/browserApi";
const BroadcasterSubscriptionId = 'TwoFactorComponent'; const BroadcasterSubscriptionId = "TwoFactorComponent";
@Component({ @Component({
selector: 'app-two-factor', selector: "app-two-factor",
templateUrl: 'two-factor.component.html', templateUrl: "two-factor.component.html",
}) })
export class TwoFactorComponent extends BaseTwoFactorComponent { export class TwoFactorComponent extends BaseTwoFactorComponent {
showNewWindowMessage = false; showNewWindowMessage = false;
constructor(authService: AuthService, router: Router, constructor(
i18nService: I18nService, apiService: ApiService, authService: AuthService,
platformUtilsService: PlatformUtilsService, private syncService: SyncService, router: Router,
environmentService: EnvironmentService, private ngZone: NgZone, i18nService: I18nService,
private broadcasterService: BroadcasterService, private changeDetectorRef: ChangeDetectorRef, apiService: ApiService,
private popupUtilsService: PopupUtilsService, stateService: StateService, route: ActivatedRoute, platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService, logService: LogService) { private syncService: SyncService,
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService, environmentService: EnvironmentService,
stateService, route, logService); 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 = () => { 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() { anotherMethod() {
if (this.route.snapshot.paramMap.has('webAuthnResponse')) { this.router.navigate(["2fa-options"]);
// 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(); async isLinux() {
if (this.selectedProviderType == null) { return (await BrowserApi.getPlatformInfo()).os === "linux";
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';
}
} }

View File

@ -1,66 +1,82 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { PolicyService } from "jslib-common/abstractions/policy.service";
import { SyncService } from 'jslib-common/abstractions/sync.service'; import { SyncService } from "jslib-common/abstractions/sync.service";
import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from 'jslib-angular/components/update-temp-password.component'; import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "jslib-angular/components/update-temp-password.component";
import { StateService } from '../../services/abstractions/state.service'; import { StateService } from "../../services/abstractions/state.service";
interface MasterPasswordScore { interface MasterPasswordScore {
Color: string; Color: string;
Text: string; Text: string;
Width: number; Width: number;
} }
@Component({ @Component({
selector: 'app-update-temp-password', selector: "app-update-temp-password",
templateUrl: 'update-temp-password.component.html', templateUrl: "update-temp-password.component.html",
}) })
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent { export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {
get masterPasswordScoreStyle(): MasterPasswordScore { get masterPasswordScoreStyle(): MasterPasswordScore {
const scoreWidth = this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20; const scoreWidth = this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
switch (this.masterPasswordScore) { switch (this.masterPasswordScore) {
case 4: case 4:
return { return {
Color: 'bg-success', Color: "bg-success",
Text: 'strong', Text: "strong",
Width: scoreWidth, Width: scoreWidth,
}; };
case 3: case 3:
return { return {
Color: 'bg-primary', Color: "bg-primary",
Text: 'good', Text: "good",
Width: scoreWidth, Width: scoreWidth,
}; };
case 2: case 2:
return { return {
Color: 'bg-warning', Color: "bg-warning",
Text: 'weak', Text: "weak",
Width: scoreWidth, Width: scoreWidth,
}; };
default: default:
return { return {
Color: 'bg-danger', Color: "bg-danger",
Text: 'weak', Text: "weak",
Width: scoreWidth, Width: scoreWidth,
}; };
}
} }
}
constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService, constructor(
passwordGenerationService: PasswordGenerationService, policyService: PolicyService, i18nService: I18nService,
cryptoService: CryptoService, stateService: StateService, platformUtilsService: PlatformUtilsService,
messagingService: MessagingService, apiService: ApiService, passwordGenerationService: PasswordGenerationService,
syncService: SyncService, logService: LogService) { policyService: PolicyService,
super(i18nService, platformUtilsService, passwordGenerationService, policyService, cryptoService: CryptoService,
cryptoService, messagingService, apiService, stateService, stateService: StateService,
syncService, logService); messagingService: MessagingService,
} apiService: ApiService,
syncService: SyncService,
logService: LogService
) {
super(
i18nService,
platformUtilsService,
passwordGenerationService,
policyService,
cryptoService,
messagingService,
apiService,
stateService,
syncService,
logService
);
}
} }

View File

@ -1,238 +1,249 @@
import { import { ChangeDetectorRef, Component, NgZone, OnInit, SecurityContext } from "@angular/core";
ChangeDetectorRef, import { DomSanitizer } from "@angular/platform-browser";
Component, import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
NgZone, import { IndividualConfig, ToastrService } from "ngx-toastr";
OnInit, import Swal, { SweetAlertIcon } from "sweetalert2/src/sweetalert2.js";
SecurityContext, import { BrowserApi } from "../browser/browserApi";
} 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 { AuthService } from "jslib-common/abstractions/auth.service";
import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StorageService } from 'jslib-common/abstractions/storage.service'; import { StorageService } from "jslib-common/abstractions/storage.service";
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { routerTransition } from './app-routing.animations'; import { routerTransition } from "./app-routing.animations";
import { StateService } from '../services/abstractions/state.service'; import { StateService } from "../services/abstractions/state.service";
@Component({ @Component({
selector: 'app-root', selector: "app-root",
styles: [], styles: [],
animations: [routerTransition], animations: [routerTransition],
template: ` template: ` <main [@routerTransition]="getState(o)">
<main [@routerTransition]="getState(o)"> <router-outlet #o="outlet"></router-outlet>
<router-outlet #o="outlet"></router-outlet> </main>`,
</main>`,
}) })
export class AppComponent implements OnInit { 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, ngOnInit() {
private broadcasterService: BroadcasterService, private authService: AuthService, if (BrowserApi.getBackgroundPage() == null) {
private i18nService: I18nService, private router: Router, return;
private stateService: StateService, private messagingService: MessagingService, }
private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone,
private sanitizer: DomSanitizer, private platformUtilsService: PlatformUtilsService,
private keyConnectoService: KeyConnectorService) { }
ngOnInit() { this.ngZone.runOutsideAngular(() => {
if (BrowserApi.getBackgroundPage() == null) { window.onmousedown = () => this.recordActivity();
return; window.ontouchstart = () => this.recordActivity();
} window.onclick = () => this.recordActivity();
window.onscroll = () => this.recordActivity();
window.onkeypress = () => this.recordActivity();
});
this.ngZone.runOutsideAngular(() => { (window as any).bitwardenPopupMainMessageListener = async (
window.onmousedown = () => this.recordActivity(); msg: any,
window.ontouchstart = () => this.recordActivity(); sender: any,
window.onclick = () => this.recordActivity(); sendResponse: any
window.onscroll = () => this.recordActivity(); ) => {
window.onkeypress = () => this.recordActivity(); 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") {
(window as any).bitwardenPopupMainMessageListener = async (msg: any, sender: any, sendResponse: any) => { this.ngZone.run(() => {
if (msg.command === 'doneLoggingOut') { this.router.navigate(["home"]);
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 === "locked") {
this.ngZone.run(() => {
getState(outlet: RouterOutlet) { this.router.navigate(["lock"]);
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<IndividualConfig> = {};
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 += ('<p>' + this.sanitizer.sanitize(SecurityContext.HTML, t) + '</p>'));
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 ? `<i class="swal-custom-icon fa ${iconClasses}"></i>` : 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 === "showDialog") {
this.messagingService.send('showDialogResolve', { await this.showDialog(msg);
dialogId: msg.dialogId, } else if (msg.command === "showToast") {
confirmed: confirmed.value, 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<IndividualConfig> = {};
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 += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>")
);
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 ? `<i class="swal-custom-icon fa ${iconClasses}"></i>` : 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,
});
}
} }

View File

@ -1,83 +1,89 @@
import { import { Component, EventEmitter, Input, Output } from "@angular/core";
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { CipherRepromptType } from 'jslib-common/enums/cipherRepromptType'; import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
import { CipherType } from 'jslib-common/enums/cipherType'; import { CipherType } from "jslib-common/enums/cipherType";
import { EventType } from 'jslib-common/enums/eventType'; 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 { EventService } from "jslib-common/abstractions/event.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service'; import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TotpService } from 'jslib-common/abstractions/totp.service'; import { TotpService } from "jslib-common/abstractions/totp.service";
import { StateService } from '../../services/abstractions/state.service'; import { StateService } from "../../services/abstractions/state.service";
@Component({ @Component({
selector: 'app-action-buttons', selector: "app-action-buttons",
templateUrl: 'action-buttons.component.html', templateUrl: "action-buttons.component.html",
}) })
export class ActionButtonsComponent { export class ActionButtonsComponent {
@Output() onView = new EventEmitter<CipherView>(); @Output() onView = new EventEmitter<CipherView>();
@Output() launchEvent = new EventEmitter<CipherView>(); @Output() launchEvent = new EventEmitter<CipherView>();
@Input() cipher: CipherView; @Input() cipher: CipherView;
@Input() showView = false; @Input() showView = false;
cipherType = CipherType; cipherType = CipherType;
userHasPremiumAccess = false; userHasPremiumAccess = false;
constructor(private i18nService: I18nService, constructor(
private platformUtilsService: PlatformUtilsService, private eventService: EventService, private i18nService: I18nService,
private totpService: TotpService, private stateService: StateService, private platformUtilsService: PlatformUtilsService,
private passwordRepromptService: PasswordRepromptService) { } private eventService: EventService,
private totpService: TotpService,
private stateService: StateService,
private passwordRepromptService: PasswordRepromptService
) {}
async ngOnInit() { async ngOnInit() {
this.userHasPremiumAccess = await this.stateService.getCanAccessPremium(); 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() { if (value == null || (aType === "TOTP" && !this.displayTotpCopyButton(cipher))) {
this.launchEvent.emit(this.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 (!cipher.viewPassword) {
if (this.cipher.reprompt !== CipherRepromptType.None && this.passwordRepromptService.protectedFields().includes(aType) && return;
!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);
}
} }
displayTotpCopyButton(cipher: CipherView) { this.platformUtilsService.copyToClipboard(value, { window: window });
return (cipher?.login?.hasTotp ?? false) && this.platformUtilsService.showToast(
(cipher.organizationUseTotp || this.userHasPremiumAccess); "info",
} null,
this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey))
);
view() { if (typeI18nKey === "password" || typeI18nKey === "verificationCodeTotp") {
this.onView.emit(this.cipher); 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);
}
} }

View File

@ -1,46 +1,48 @@
import { Location } from '@angular/common'; import { Location } from "@angular/common";
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from '../../services/abstractions/state.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 { import { PasswordGeneratorComponent as BasePasswordGeneratorComponent } from "jslib-angular/components/password-generator.component";
PasswordGeneratorComponent as BasePasswordGeneratorComponent,
} from 'jslib-angular/components/password-generator.component';
@Component({ @Component({
selector: 'app-password-generator', selector: "app-password-generator",
templateUrl: 'password-generator.component.html', templateUrl: "password-generator.component.html",
}) })
export class PasswordGeneratorComponent extends BasePasswordGeneratorComponent { export class PasswordGeneratorComponent extends BasePasswordGeneratorComponent {
private cipherState: CipherView; private cipherState: CipherView;
constructor(passwordGenerationService: PasswordGenerationService, platformUtilsService: PlatformUtilsService, constructor(
i18nService: I18nService, private stateService: StateService, passwordGenerationService: PasswordGenerationService,
private location: Location) { platformUtilsService: PlatformUtilsService,
super(passwordGenerationService, platformUtilsService, i18nService, window); i18nService: I18nService,
} private stateService: StateService,
private location: Location
) {
super(passwordGenerationService, platformUtilsService, i18nService, window);
}
async ngOnInit() { async ngOnInit() {
await super.ngOnInit(); await super.ngOnInit();
const addEditCipherInfo = await this.stateService.getAddEditCipherInfo(); const addEditCipherInfo = await this.stateService.getAddEditCipherInfo();
if (addEditCipherInfo != null) { if (addEditCipherInfo != null) {
this.cipherState = addEditCipherInfo.cipher; this.cipherState = addEditCipherInfo.cipher;
}
this.showSelect = this.cipherState != null;
} }
this.showSelect = this.cipherState != null;
}
select() { select() {
super.select(); super.select();
this.cipherState.login.password = this.password; this.cipherState.login.password = this.password;
this.close(); this.close();
} }
close() { close() {
this.location.back(); this.location.back();
} }
} }

View File

@ -1,129 +1,150 @@
import { import { DatePipe, Location } from "@angular/common";
DatePipe,
Location,
} from '@angular/common';
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { import { ActivatedRoute, Router } from "@angular/router";
ActivatedRoute,
Router,
} from '@angular/router';
import { first } from 'rxjs/operators'; import { first } from "rxjs/operators";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { PolicyService } from "jslib-common/abstractions/policy.service";
import { SendService } from 'jslib-common/abstractions/send.service'; import { SendService } from "jslib-common/abstractions/send.service";
import { TokenService } from 'jslib-common/abstractions/token.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 { AddEditComponent as BaseAddEditComponent } from "jslib-angular/components/send/add-edit.component";
import { StateService } from '../../services/abstractions/state.service'; import { StateService } from "../../services/abstractions/state.service";
@Component({ @Component({
selector: 'app-send-add-edit', selector: "app-send-add-edit",
templateUrl: 'send-add-edit.component.html', templateUrl: "send-add-edit.component.html",
}) })
export class SendAddEditComponent extends BaseAddEditComponent { export class SendAddEditComponent extends BaseAddEditComponent {
// Options header // Options header
showOptions = false; showOptions = false;
// File visibility // File visibility
isFirefox = false; isFirefox = false;
inPopout = false; inPopout = false;
inSidebar = false; inSidebar = false;
isLinux = false; isLinux = false;
isUnsupportedMac = false; isUnsupportedMac = false;
constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService, constructor(
stateService: StateService, messagingService: MessagingService, policyService: PolicyService, i18nService: I18nService,
environmentService: EnvironmentService, datePipe: DatePipe, sendService: SendService, platformUtilsService: PlatformUtilsService,
private route: ActivatedRoute, private router: Router, private location: Location, stateService: StateService,
private popupUtilsService: PopupUtilsService, logService: LogService) { messagingService: MessagingService,
super(i18nService, platformUtilsService, environmentService, datePipe, policyService: PolicyService,
sendService, messagingService, policyService, logService, stateService); 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<boolean> {
if (await super.submit()) {
this.cancel();
return true;
} }
get showFileSelector(): boolean { return false;
return !(this.editMode || this.showFilePopoutMessage); }
async delete(): Promise<boolean> {
if (await super.delete()) {
this.cancel();
return true;
} }
get showFilePopoutMessage(): boolean { return false;
return !this.editMode && (this.showFirefoxFileWarning || this.showSafariFileWarning || this.showChromiumFileWarning); }
}
cancel() {
get showFirefoxFileWarning(): boolean { // If true, the window was pop'd out on the add-send page. location.back will not work
return this.isFirefox && !(this.inSidebar || this.inPopout); if ((window as any).previousPopupUrl.startsWith("/add-send")) {
} this.router.navigate(["tabs/send"]);
} else {
get showSafariFileWarning(): boolean { this.location.back();
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<boolean> {
if (await super.submit()) {
this.cancel();
return true;
}
return false;
}
async delete(): Promise<boolean> {
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();
}
} }
}
} }

View File

@ -1,185 +1,200 @@
import { import { ChangeDetectorRef, Component, NgZone } from "@angular/core";
ChangeDetectorRef,
Component,
NgZone,
} from '@angular/core';
import { import { Router } from "@angular/router";
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 { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { PolicyService } from "jslib-common/abstractions/policy.service";
import { SearchService } from 'jslib-common/abstractions/search.service'; import { SearchService } from "jslib-common/abstractions/search.service";
import { SendService } from 'jslib-common/abstractions/send.service'; import { SendService } from "jslib-common/abstractions/send.service";
import { SyncService } from 'jslib-common/abstractions/sync.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 { SendType } from "jslib-common/enums/sendType";
import { BrowserSendComponentState } from '../../models/browserSendComponentState'; import { BrowserSendComponentState } from "../../models/browserSendComponentState";
const ComponentId = 'SendComponent'; const ComponentId = "SendComponent";
const ScopeStateId = ComponentId + 'Scope'; const ScopeStateId = ComponentId + "Scope";
@Component({ @Component({
selector: 'app-send-groupings', selector: "app-send-groupings",
templateUrl: 'send-groupings.component.html', templateUrl: "send-groupings.component.html",
}) })
export class SendGroupingsComponent extends BaseSendComponent { export class SendGroupingsComponent extends BaseSendComponent {
// Header // Header
showLeftHeader = true; showLeftHeader = true;
// Send Type Calculations // Send Type Calculations
typeCounts = new Map<SendType, number>(); typeCounts = new Map<SendType, number>();
// State Handling // State Handling
state: BrowserSendComponentState; state: BrowserSendComponentState;
private loadedTimeout: number; private loadedTimeout: number;
constructor(sendService: SendService, i18nService: I18nService, constructor(
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, ngZone: NgZone, sendService: SendService,
policyService: PolicyService, searchService: SearchService, i18nService: I18nService,
private popupUtils: PopupUtilsService, private stateService: StateService, platformUtilsService: PlatformUtilsService,
private router: Router, private syncService: SyncService, environmentService: EnvironmentService,
private changeDetectorRef: ChangeDetectorRef, private broadcasterService: BroadcasterService, ngZone: NgZone,
logService: LogService) { policyService: PolicyService,
super(sendService, i18nService, platformUtilsService, environmentService, ngZone, searchService, searchService: SearchService,
policyService, logService); private popupUtils: PopupUtilsService,
super.onSuccessfulLoad = async () => { private stateService: StateService,
this.calculateTypeCounts(); private router: Router,
this.selectAll(); 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() { if (!this.syncService.syncInProgress) {
// Determine Header details this.load();
this.showLeftHeader = !(this.popupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()); } else {
// Clear state of Send Type Component this.loadedTimeout = window.setTimeout(() => {
this.stateService.setBrowserSendComponentState(null); if (!this.loaded) {
// Let super class finish this.load();
await super.ngOnInit(); }
// Handle State Restore if necessary }, 5000);
const restoredScopeState = await this.restoreState(); }
if (this.state?.searchText != null) {
this.searchText = this.state.searchText; 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.changeDetectorRef.detectChanges();
this.load(); });
} else { });
this.loadedTimeout = window.setTimeout(() => { }
if (!this.loaded) {
this.load();
}
}, 5000);
}
if (!this.syncService.syncInProgress || restoredScopeState) { ngOnDestroy() {
window.setTimeout(() => this.popupUtils.setContentScrollY(window, this.state.scrollY), 0); // 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 async selectType(type: SendType) {
this.broadcasterService.subscribe(ComponentId, (message: any) => { this.router.navigate(["/send-type"], { queryParams: { type: type } });
this.ngZone.run(async () => { }
switch (message.command) {
case 'syncCompleted':
window.setTimeout(() => {
this.load();
}, 500);
break;
default:
break;
}
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<boolean> {
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<SendType, number>();
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<boolean> {
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() { return true;
// 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<boolean> {
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<SendType, number>();
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<boolean> {
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;
}
} }

View File

@ -1,158 +1,170 @@
import { import { ChangeDetectorRef, Component, NgZone } from "@angular/core";
ChangeDetectorRef,
Component,
NgZone,
} from '@angular/core';
import { import { ActivatedRoute, Router } from "@angular/router";
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 { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { PolicyService } from "jslib-common/abstractions/policy.service";
import { SearchService } from 'jslib-common/abstractions/search.service'; import { SearchService } from "jslib-common/abstractions/search.service";
import { SendService } from 'jslib-common/abstractions/send.service'; import { SendService } from "jslib-common/abstractions/send.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 { SendType } from "jslib-common/enums/sendType";
import { BrowserComponentState } from '../../models/browserComponentState'; import { BrowserComponentState } from "../../models/browserComponentState";
const ComponentId = 'SendTypeComponent'; const ComponentId = "SendTypeComponent";
@Component({ @Component({
selector: 'app-send-type', selector: "app-send-type",
templateUrl: 'send-type.component.html', templateUrl: "send-type.component.html",
}) })
export class SendTypeComponent extends BaseSendComponent { export class SendTypeComponent extends BaseSendComponent {
groupingTitle: string; groupingTitle: string;
// State Handling // State Handling
state: BrowserComponentState; state: BrowserComponentState;
private refreshTimeout: number; private refreshTimeout: number;
private applySavedState = true; private applySavedState = true;
constructor(sendService: SendService, i18nService: I18nService, constructor(
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, ngZone: NgZone, sendService: SendService,
policyService: PolicyService, searchService: SearchService, i18nService: I18nService,
private popupUtils: PopupUtilsService, private stateService: StateService, platformUtilsService: PlatformUtilsService,
private route: ActivatedRoute, private location: Location, private changeDetectorRef: ChangeDetectorRef, environmentService: EnvironmentService,
private broadcasterService: BroadcasterService, private router: Router, logService: LogService) { ngZone: NgZone,
super(sendService, i18nService, platformUtilsService, environmentService, ngZone, searchService, policyService: PolicyService,
policyService, logService); searchService: SearchService,
super.onSuccessfulLoad = async () => { private popupUtils: PopupUtilsService,
this.selectType(this.type); private stateService: StateService,
}; private route: ActivatedRoute,
this.applySavedState = (window as any).previousPopupUrl != null && private location: Location,
!(window as any).previousPopupUrl.startsWith('/send-type'); 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() { async ngOnInit() {
// Let super class finish // Let super class finish
await super.ngOnInit(); await super.ngOnInit();
this.route.queryParams.pipe(first()).subscribe(async params => { this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (this.applySavedState) { if (this.applySavedState) {
this.state = (await this.stateService.getBrowserSendTypeComponentState()); this.state = await this.stateService.getBrowserSendTypeComponentState();
if (this.state.searchText != null) { if (this.state.searchText != null) {
this.searchText = this.state.searchText; 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);
} }
// Save state }
this.saveState();
// Unsubscribe
this.broadcasterService.unsubscribe(ComponentId);
}
async selectSend(s: SendView) { if (params.type != null) {
this.router.navigate(['/edit-send'], { queryParams: { sendId: s.id } }); this.type = parseInt(params.type, null);
} switch (this.type) {
case SendType.Text:
async addSend() { this.groupingTitle = this.i18nService.t("sendTypeText");
if (this.disableSend) { break;
return; 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<boolean> { // Restore state and remove reference
if (this.disableSend) { if (this.applySavedState && this.state != null) {
return; 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() { this.changeDetectorRef.detectChanges();
(window as any).routeDirection = 'b'; });
this.location.back(); });
} }
private async saveState() { ngOnDestroy() {
this.state = { // Remove timeout
scrollY: this.popupUtils.getContentScrollY(window), if (this.refreshTimeout != null) {
searchText: this.searchText, window.clearTimeout(this.refreshTimeout);
};
await this.stateService.setBrowserSendTypeComponentState(this.state);
} }
// 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<boolean> {
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);
}
} }

View File

@ -1,223 +1,282 @@
import { import { APP_INITIALIZER, LOCALE_ID, NgModule } from "@angular/core";
APP_INITIALIZER,
LOCALE_ID,
NgModule,
} from '@angular/core';
import { DebounceNavigationService } from './debounceNavigationService'; import { DebounceNavigationService } from "./debounceNavigationService";
import { LaunchGuardService } from './launch-guard.service'; import { LaunchGuardService } from "./launch-guard.service";
import { LockGuardService } from './lock-guard.service'; import { LockGuardService } from "./lock-guard.service";
import { PasswordRepromptService } from './password-reprompt.service'; import { PasswordRepromptService } from "./password-reprompt.service";
import { UnauthGuardService } from './unauth-guard.service'; import { UnauthGuardService } from "./unauth-guard.service";
import { JslibServicesModule } from 'jslib-angular/services/jslib-services.module'; import { JslibServicesModule } from "jslib-angular/services/jslib-services.module";
import { LockGuardService as BaseLockGuardService } from 'jslib-angular/services/lock-guard.service'; import { LockGuardService as BaseLockGuardService } from "jslib-angular/services/lock-guard.service";
import { UnauthGuardService as BaseUnauthGuardService } from 'jslib-angular/services/unauth-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 { ApiService } from "jslib-common/abstractions/api.service";
import { AppIdService } from 'jslib-common/abstractions/appId.service'; import { AppIdService } from "jslib-common/abstractions/appId.service";
import { AuditService } from 'jslib-common/abstractions/audit.service'; import { AuditService } from "jslib-common/abstractions/audit.service";
import { AuthService as AuthServiceAbstraction } from 'jslib-common/abstractions/auth.service'; import { AuthService as AuthServiceAbstraction } from "jslib-common/abstractions/auth.service";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from 'jslib-common/abstractions/collection.service'; import { CollectionService } from "jslib-common/abstractions/collection.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { EventService } from 'jslib-common/abstractions/event.service'; import { EventService } from "jslib-common/abstractions/event.service";
import { ExportService } from 'jslib-common/abstractions/export.service'; import { ExportService } from "jslib-common/abstractions/export.service";
import { FileUploadService } from 'jslib-common/abstractions/fileUpload.service'; import { FileUploadService } from "jslib-common/abstractions/fileUpload.service";
import { FolderService } from 'jslib-common/abstractions/folder.service'; import { FolderService } from "jslib-common/abstractions/folder.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { LogService as LogServiceAbstraction } from 'jslib-common/abstractions/log.service'; import { LogService as LogServiceAbstraction } from "jslib-common/abstractions/log.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { NotificationsService } from 'jslib-common/abstractions/notifications.service'; import { NotificationsService } from "jslib-common/abstractions/notifications.service";
import { OrganizationService } from 'jslib-common/abstractions/organization.service'; import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from 'jslib-common/abstractions/passwordReprompt.service'; import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { PolicyService } from "jslib-common/abstractions/policy.service";
import { ProviderService } from 'jslib-common/abstractions/provider.service'; import { ProviderService } from "jslib-common/abstractions/provider.service";
import { SearchService as SearchServiceAbstraction } from 'jslib-common/abstractions/search.service'; import { SearchService as SearchServiceAbstraction } from "jslib-common/abstractions/search.service";
import { SendService } from 'jslib-common/abstractions/send.service'; import { SendService } from "jslib-common/abstractions/send.service";
import { SettingsService } from 'jslib-common/abstractions/settings.service'; import { SettingsService } from "jslib-common/abstractions/settings.service";
import { StorageService } from 'jslib-common/abstractions/storage.service'; import { StorageService } from "jslib-common/abstractions/storage.service";
import { SyncService } from 'jslib-common/abstractions/sync.service'; import { SyncService } from "jslib-common/abstractions/sync.service";
import { TokenService } from 'jslib-common/abstractions/token.service'; import { TokenService } from "jslib-common/abstractions/token.service";
import { TotpService } from 'jslib-common/abstractions/totp.service'; import { TotpService } from "jslib-common/abstractions/totp.service";
import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service'; import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { AutofillService } from '../../services/abstractions/autofill.service'; import { AutofillService } from "../../services/abstractions/autofill.service";
import BrowserMessagingService from '../../services/browserMessaging.service'; import BrowserMessagingService from "../../services/browserMessaging.service";
import { AuthService } from 'jslib-common/services/auth.service'; import { AuthService } from "jslib-common/services/auth.service";
import { ConsoleLogService } from 'jslib-common/services/consoleLog.service'; import { ConsoleLogService } from "jslib-common/services/consoleLog.service";
import { SearchService } from 'jslib-common/services/search.service'; import { SearchService } from "jslib-common/services/search.service";
import { PopupSearchService } from './popup-search.service'; import { PopupSearchService } from "./popup-search.service";
import { PopupUtilsService } from './popup-utils.service'; import { PopupUtilsService } from "./popup-utils.service";
import { ThemeType } from 'jslib-common/enums/themeType'; import { ThemeType } from "jslib-common/enums/themeType";
import { StateService } from '../../services/state.service'; 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<T>(service: string) { function getBgService<T>(service: string) {
return (): T => { return (): T => {
const page = BrowserApi.getBackgroundPage(); const page = BrowserApi.getBackgroundPage();
return page ? page.bitwardenMain[service] as T : null; return page ? (page.bitwardenMain[service] as T) : null;
}; };
} }
const isPrivateMode = BrowserApi.getBackgroundPage() == null; const isPrivateMode = BrowserApi.getBackgroundPage() == null;
export function initFactory(platformUtilsService: PlatformUtilsService, i18nService: I18nService, export function initFactory(
storageService: StorageService, popupUtilsService: PopupUtilsService, stateService: StateServiceAbstraction, platformUtilsService: PlatformUtilsService,
logService: LogServiceAbstraction): Function { i18nService: I18nService,
return async () => { storageService: StorageService,
if (!popupUtilsService.inPopup(window)) { popupUtilsService: PopupUtilsService,
window.document.body.classList.add('body-full'); stateService: StateServiceAbstraction,
} else if (window.screen.availHeight < 600) { logService: LogServiceAbstraction
window.document.body.classList.add('body-xs'); ): Function {
} else if (window.screen.availHeight <= 800) { return async () => {
window.document.body.classList.add('body-sm'); 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) { if (!isPrivateMode) {
const htmlEl = window.document.documentElement; const htmlEl = window.document.documentElement;
const theme = await platformUtilsService.getEffectiveTheme(); const theme = await platformUtilsService.getEffectiveTheme();
htmlEl.classList.add('theme_' + theme); htmlEl.classList.add("theme_" + theme);
platformUtilsService.onDefaultSystemThemeChange(async sysTheme => { platformUtilsService.onDefaultSystemThemeChange(async (sysTheme) => {
const bwTheme = await stateService.getTheme(); const bwTheme = await stateService.getTheme();
if (bwTheme == null || bwTheme === ThemeType.System) { if (bwTheme == null || bwTheme === ThemeType.System) {
htmlEl.classList.remove('theme_' + ThemeType.Light, 'theme_' + ThemeType.Dark); htmlEl.classList.remove("theme_" + ThemeType.Light, "theme_" + ThemeType.Dark);
htmlEl.classList.add('theme_' + sysTheme); 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');
}
} }
}; });
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({ @NgModule({
imports: [ imports: [JslibServicesModule],
JslibServicesModule, declarations: [],
], providers: [
declarations: [], {
providers: [ provide: LOCALE_ID,
{ useFactory: () =>
provide: LOCALE_ID, isPrivateMode ? null : getBgService<I18nService>("i18nService")().translationLocale,
useFactory: () => isPrivateMode ? null : getBgService<I18nService>('i18nService')().translationLocale, deps: [],
deps: [], },
}, {
{ provide: APP_INITIALIZER,
provide: APP_INITIALIZER, useFactory: initFactory,
useFactory: initFactory, deps: [
deps: [ PlatformUtilsService,
PlatformUtilsService, I18nService,
I18nService, StorageService,
StorageService,
PopupUtilsService,
StateServiceAbstraction,
LogServiceAbstraction,
],
multi: true,
},
LaunchGuardService,
{ provide: BaseLockGuardService, useClass: LockGuardService },
{ provide: BaseUnauthGuardService, useClass: UnauthGuardService },
DebounceNavigationService,
PopupUtilsService, PopupUtilsService,
{ provide: MessagingService, useClass: BrowserMessagingService }, StateServiceAbstraction,
{ provide: AuthServiceAbstraction, useFactory: getBgService<AuthService>('authService'), deps: [] }, LogServiceAbstraction,
{ provide: StateServiceAbstraction, useFactory: getBgService<StateService>('stateService') }, ],
{ multi: true,
provide: SearchServiceAbstraction, },
useFactory: (cipherService: CipherService, logService: ConsoleLogService, i18nService: I18nService) => { LaunchGuardService,
return isPrivateMode ? null : new PopupSearchService(getBgService<SearchService>('searchService')(), { provide: BaseLockGuardService, useClass: LockGuardService },
cipherService, logService, i18nService); { provide: BaseUnauthGuardService, useClass: UnauthGuardService },
}, DebounceNavigationService,
deps: [ PopupUtilsService,
CipherService, { provide: MessagingService, useClass: BrowserMessagingService },
LogServiceAbstraction, {
I18nService, provide: AuthServiceAbstraction,
], useFactory: getBgService<AuthService>("authService"),
}, deps: [],
{ provide: AuditService, useFactory: getBgService<AuditService>('auditService'), deps: [] }, },
{ provide: FileUploadService, useFactory: getBgService<FileUploadService>('fileUploadService'), deps: [] }, { provide: StateServiceAbstraction, useFactory: getBgService<StateService>("stateService") },
{ provide: CipherService, useFactory: getBgService<CipherService>('cipherService'), deps: [] }, {
{ provide: SearchServiceAbstraction,
provide: CryptoFunctionService, useFactory: (
useFactory: getBgService<CryptoFunctionService>('cryptoFunctionService'), cipherService: CipherService,
deps: [], logService: ConsoleLogService,
}, i18nService: I18nService
{ provide: FolderService, useFactory: getBgService<FolderService>('folderService'), deps: [] }, ) => {
{ provide: CollectionService, useFactory: getBgService<CollectionService>('collectionService'), deps: [] }, return isPrivateMode
{ provide: LogServiceAbstraction, useFactory: getBgService<ConsoleLogService>('logService'), deps: [] }, ? null
{ provide: EnvironmentService, useFactory: getBgService<EnvironmentService>('environmentService'), deps: [] }, : new PopupSearchService(
{ provide: TotpService, useFactory: getBgService<TotpService>('totpService'), deps: [] }, getBgService<SearchService>("searchService")(),
{ provide: TokenService, useFactory: getBgService<TokenService>('tokenService'), deps: [] }, cipherService,
{ provide: I18nService, useFactory: getBgService<I18nService>('i18nService'), deps: [] }, logService,
{ provide: CryptoService, useFactory: getBgService<CryptoService>('cryptoService'), deps: [] }, i18nService
{ provide: EventService, useFactory: getBgService<EventService>('eventService'), deps: [] }, );
{ provide: PolicyService, useFactory: getBgService<PolicyService>('policyService'), deps: [] }, },
{ deps: [CipherService, LogServiceAbstraction, I18nService],
provide: PlatformUtilsService, },
useFactory: getBgService<PlatformUtilsService>('platformUtilsService'), { provide: AuditService, useFactory: getBgService<AuditService>("auditService"), deps: [] },
deps: [], {
}, provide: FileUploadService,
{ useFactory: getBgService<FileUploadService>("fileUploadService"),
provide: PasswordGenerationService, deps: [],
useFactory: getBgService<PasswordGenerationService>('passwordGenerationService'), },
deps: [], { provide: CipherService, useFactory: getBgService<CipherService>("cipherService"), deps: [] },
}, {
{ provide: ApiService, useFactory: getBgService<ApiService>('apiService'), deps: [] }, provide: CryptoFunctionService,
{ provide: SyncService, useFactory: getBgService<SyncService>('syncService'), deps: [] }, useFactory: getBgService<CryptoFunctionService>("cryptoFunctionService"),
{ provide: SettingsService, useFactory: getBgService<SettingsService>('settingsService'), deps: [] }, deps: [],
{ provide: StorageService, useFactory: getBgService<StorageService>('storageService'), deps: [] }, },
{ provide: AppIdService, useFactory: getBgService<AppIdService>('appIdService'), deps: [] }, { provide: FolderService, useFactory: getBgService<FolderService>("folderService"), deps: [] },
{ provide: AutofillService, useFactory: getBgService<AutofillService>('autofillService'), deps: [] }, {
{ provide: ExportService, useFactory: getBgService<ExportService>('exportService'), deps: [] }, provide: CollectionService,
{ provide: SendService, useFactory: getBgService<SendService>('sendService'), deps: [] }, useFactory: getBgService<CollectionService>("collectionService"),
{ provide: KeyConnectorService, useFactory: getBgService<KeyConnectorService>('keyConnectorService'), deps: [] }, deps: [],
{ },
provide: UserVerificationService, {
useFactory: getBgService<UserVerificationService>('userVerificationService'), provide: LogServiceAbstraction,
deps: [], useFactory: getBgService<ConsoleLogService>("logService"),
}, deps: [],
{ },
provide: VaultTimeoutService, {
useFactory: getBgService<VaultTimeoutService>('vaultTimeoutService'), provide: EnvironmentService,
deps: [], useFactory: getBgService<EnvironmentService>("environmentService"),
}, deps: [],
{ },
provide: NotificationsService, { provide: TotpService, useFactory: getBgService<TotpService>("totpService"), deps: [] },
useFactory: getBgService<NotificationsService>('notificationsService'), { provide: TokenService, useFactory: getBgService<TokenService>("tokenService"), deps: [] },
deps: [], { provide: I18nService, useFactory: getBgService<I18nService>("i18nService"), deps: [] },
}, { provide: CryptoService, useFactory: getBgService<CryptoService>("cryptoService"), deps: [] },
{ provide: LogServiceAbstraction, useFactory: getBgService<ConsoleLogService>('logService'), deps: [] }, { provide: EventService, useFactory: getBgService<EventService>("eventService"), deps: [] },
{ provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService }, { provide: PolicyService, useFactory: getBgService<PolicyService>("policyService"), deps: [] },
{ provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService }, {
{ provide: OrganizationService, useFactory: getBgService<OrganizationService>('organizationService'), deps: [] }, provide: PlatformUtilsService,
{ provide: ProviderService, useFactory: getBgService<ProviderService>('providerService'), deps: [] }, useFactory: getBgService<PlatformUtilsService>("platformUtilsService"),
{ provide: 'SECURE_STORAGE', useFactory: getBgService<StorageService>('secureStorageService'), deps: [] }, deps: [],
], },
{
provide: PasswordGenerationService,
useFactory: getBgService<PasswordGenerationService>("passwordGenerationService"),
deps: [],
},
{ provide: ApiService, useFactory: getBgService<ApiService>("apiService"), deps: [] },
{ provide: SyncService, useFactory: getBgService<SyncService>("syncService"), deps: [] },
{
provide: SettingsService,
useFactory: getBgService<SettingsService>("settingsService"),
deps: [],
},
{
provide: StorageService,
useFactory: getBgService<StorageService>("storageService"),
deps: [],
},
{ provide: AppIdService, useFactory: getBgService<AppIdService>("appIdService"), deps: [] },
{
provide: AutofillService,
useFactory: getBgService<AutofillService>("autofillService"),
deps: [],
},
{ provide: ExportService, useFactory: getBgService<ExportService>("exportService"), deps: [] },
{ provide: SendService, useFactory: getBgService<SendService>("sendService"), deps: [] },
{
provide: KeyConnectorService,
useFactory: getBgService<KeyConnectorService>("keyConnectorService"),
deps: [],
},
{
provide: UserVerificationService,
useFactory: getBgService<UserVerificationService>("userVerificationService"),
deps: [],
},
{
provide: VaultTimeoutService,
useFactory: getBgService<VaultTimeoutService>("vaultTimeoutService"),
deps: [],
},
{
provide: NotificationsService,
useFactory: getBgService<NotificationsService>("notificationsService"),
deps: [],
},
{
provide: LogServiceAbstraction,
useFactory: getBgService<ConsoleLogService>("logService"),
deps: [],
},
{ provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService },
{ provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService },
{
provide: OrganizationService,
useFactory: getBgService<OrganizationService>("organizationService"),
deps: [],
},
{
provide: ProviderService,
useFactory: getBgService<ProviderService>("providerService"),
deps: [],
},
{
provide: "SECURE_STORAGE",
useFactory: getBgService<StorageService>("secureStorageService"),
deps: [],
},
],
}) })
export class ServicesModule { export class ServicesModule {}
}

View File

@ -1,114 +1,118 @@
import { import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
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 { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.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 { interface ExcludedDomain {
uri: string; uri: string;
showCurrentUris: boolean; showCurrentUris: boolean;
} }
const BroadcasterSubscriptionId = 'excludedDomains'; const BroadcasterSubscriptionId = "excludedDomains";
@Component({ @Component({
selector: 'app-excluded-domains', selector: "app-excluded-domains",
templateUrl: 'excluded-domains.component.html', templateUrl: "excluded-domains.component.html",
}) })
export class ExcludedDomainsComponent implements OnInit, OnDestroy { export class ExcludedDomainsComponent implements OnInit, OnDestroy {
excludedDomains: ExcludedDomain[] = []; excludedDomains: ExcludedDomain[] = [];
currentUris: string[]; currentUris: string[];
loadCurrentUrisTimeout: number; loadCurrentUrisTimeout: number;
constructor(private stateService: StateService, constructor(
private i18nService: I18nService, private router: Router, private stateService: StateService,
private broadcasterService: BroadcasterService, private ngZone: NgZone, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService) { 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() { await this.loadCurrentUris();
const savedDomains = await this.stateService.getNeverDomains();
if (savedDomains) { this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
for (const uri of Object.keys(savedDomains)) { this.ngZone.run(async () => {
this.excludedDomains.push({ uri: uri, showCurrentUris: false }); 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) => { async addUri() {
this.ngZone.run(async () => { this.excludedDomains.push({ uri: "", showCurrentUris: false });
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;
}
});
});
}
ngOnDestroy() { async removeUri(i: number) {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); this.excludedDomains.splice(i, 1);
} }
async addUri() { async submit() {
this.excludedDomains.push({ uri: '', showCurrentUris: false }); const savedDomains: { [name: string]: null } = {};
} for (const domain of this.excludedDomains) {
if (domain.uri && domain.uri !== "") {
async removeUri(i: number) { const validDomain = Utils.getHostname(domain.uri);
this.excludedDomains.splice(i, 1); if (!validDomain) {
} this.platformUtilsService.showToast(
"error",
async submit() { null,
const savedDomains: { [name: string]: null } = {}; this.i18nService.t("excludedDomainsInvalidDomain", domain.uri)
for (const domain of this.excludedDomains) { );
if (domain.uri && domain.uri !== '') { return;
const validDomain = Utils.getHostname(domain.uri);
if (!validDomain) {
this.platformUtilsService.showToast('error', null,
this.i18nService.t('excludedDomainsInvalidDomain', domain.uri));
return;
}
savedDomains[validDomain] = null;
}
} }
await this.stateService.setNeverDomains(savedDomains); savedDomains[validDomain] = null;
this.router.navigate(['/tabs/settings']); }
} }
await this.stateService.setNeverDomains(savedDomains);
this.router.navigate(["/tabs/settings"]);
}
trackByFunction(index: number, item: any) { trackByFunction(index: number, item: any) {
return index; return index;
} }
toggleUriInput(domain: ExcludedDomain) { toggleUriInput(domain: ExcludedDomain) {
domain.showCurrentUris = !domain.showCurrentUris; domain.showCurrentUris = !domain.showCurrentUris;
} }
async loadCurrentUris() { async loadCurrentUris() {
const tabs = await BrowserApi.tabsQuery({ windowType: 'normal' }); const tabs = await BrowserApi.tabsQuery({ windowType: "normal" });
if (tabs) { if (tabs) {
const uriSet = new Set(tabs.map(tab => Utils.getHostname(tab.url))); const uriSet = new Set(tabs.map((tab) => Utils.getHostname(tab.url)));
uriSet.delete(null); uriSet.delete(null);
this.currentUris = Array.from(uriSet); this.currentUris = Array.from(uriSet);
}
} }
}
} }

View File

@ -1,156 +1,161 @@
import { import { Component, OnInit } from "@angular/core";
Component,
OnInit,
} from '@angular/core';
import { ThemeType } from 'jslib-common/enums/themeType'; import { ThemeType } from "jslib-common/enums/themeType";
import { UriMatchType } from 'jslib-common/enums/uriMatchType'; import { UriMatchType } from "jslib-common/enums/uriMatchType";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { TotpService } from 'jslib-common/abstractions/totp.service'; import { TotpService } from "jslib-common/abstractions/totp.service";
import { StateService } from '../../services/abstractions/state.service'; import { StateService } from "../../services/abstractions/state.service";
@Component({ @Component({
selector: 'app-options', selector: "app-options",
templateUrl: 'options.component.html', templateUrl: "options.component.html",
}) })
export class OptionsComponent implements OnInit { export class OptionsComponent implements OnInit {
disableFavicon = false; disableFavicon = false;
disableBadgeCounter = false; disableBadgeCounter = false;
enableAutoFillOnPageLoad = false; enableAutoFillOnPageLoad = false;
autoFillOnPageLoadDefault = false; autoFillOnPageLoadDefault = false;
autoFillOnPageLoadOptions: any[]; autoFillOnPageLoadOptions: any[];
disableAutoTotpCopy = false; disableAutoTotpCopy = false;
disableContextMenuItem = false; disableContextMenuItem = false;
disableAddLoginNotification = false; disableAddLoginNotification = false;
disableChangedPasswordNotification = false; disableChangedPasswordNotification = false;
dontShowCards = false; dontShowCards = false;
dontShowIdentities = false; dontShowIdentities = false;
showClearClipboard = true; showClearClipboard = true;
theme: string; theme: string;
themeOptions: any[]; themeOptions: any[];
defaultUriMatch = UriMatchType.Domain; defaultUriMatch = UriMatchType.Domain;
uriMatchOptions: any[]; uriMatchOptions: any[];
clearClipboard: number; clearClipboard: number;
clearClipboardOptions: any[]; clearClipboardOptions: any[];
showGeneral: boolean = true; showGeneral: boolean = true;
showAutofill: boolean = true; showAutofill: boolean = true;
showDisplay: boolean = true; showDisplay: boolean = true;
constructor(private messagingService: MessagingService, private stateService: StateService, constructor(
private totpService: TotpService, i18nService: I18nService) { private messagingService: MessagingService,
this.themeOptions = [ private stateService: StateService,
{ name: i18nService.t('default'), value: null }, private totpService: TotpService,
{ name: i18nService.t('light'), value: ThemeType.Light }, i18nService: I18nService
{ name: i18nService.t('dark'), value: ThemeType.Dark }, ) {
{ name: 'Nord', value: ThemeType.Nord }, this.themeOptions = [
{ name: i18nService.t('solarizedDark'), value: ThemeType.SolarizedDark }, { name: i18nService.t("default"), value: null },
]; { name: i18nService.t("light"), value: ThemeType.Light },
this.uriMatchOptions = [ { name: i18nService.t("dark"), value: ThemeType.Dark },
{ name: i18nService.t('baseDomain'), value: UriMatchType.Domain }, { name: "Nord", value: ThemeType.Nord },
{ name: i18nService.t('host'), value: UriMatchType.Host }, { name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
{ name: i18nService.t('startsWith'), value: UriMatchType.StartsWith }, ];
{ name: i18nService.t('regEx'), value: UriMatchType.RegularExpression }, this.uriMatchOptions = [
{ name: i18nService.t('exact'), value: UriMatchType.Exact }, { name: i18nService.t("baseDomain"), value: UriMatchType.Domain },
{ name: i18nService.t('never'), value: UriMatchType.Never }, { name: i18nService.t("host"), value: UriMatchType.Host },
]; { name: i18nService.t("startsWith"), value: UriMatchType.StartsWith },
this.clearClipboardOptions = [ { name: i18nService.t("regEx"), value: UriMatchType.RegularExpression },
{ name: i18nService.t('never'), value: null }, { name: i18nService.t("exact"), value: UriMatchType.Exact },
{ name: i18nService.t('tenSeconds'), value: 10 }, { name: i18nService.t("never"), value: UriMatchType.Never },
{ name: i18nService.t('twentySeconds'), value: 20 }, ];
{ name: i18nService.t('thirtySeconds'), value: 30 }, this.clearClipboardOptions = [
{ name: i18nService.t('oneMinute'), value: 60 }, { name: i18nService.t("never"), value: null },
{ name: i18nService.t('twoMinutes'), value: 120 }, { name: i18nService.t("tenSeconds"), value: 10 },
{ name: i18nService.t('fiveMinutes'), value: 300 }, { name: i18nService.t("twentySeconds"), value: 20 },
]; { name: i18nService.t("thirtySeconds"), value: 30 },
this.autoFillOnPageLoadOptions = [ { name: i18nService.t("oneMinute"), value: 60 },
{ name: i18nService.t('autoFillOnPageLoadYes'), value: true }, { name: i18nService.t("twoMinutes"), value: 120 },
{ name: i18nService.t('autoFillOnPageLoadNo'), value: false }, { name: i18nService.t("fiveMinutes"), value: 300 },
]; ];
} this.autoFillOnPageLoadOptions = [
{ name: i18nService.t("autoFillOnPageLoadYes"), value: true },
{ name: i18nService.t("autoFillOnPageLoadNo"), value: false },
];
}
async ngOnInit() { async ngOnInit() {
this.enableAutoFillOnPageLoad = await this.stateService.getEnableAutoFillOnPageLoad(); 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.dontShowCards = await this.stateService.getDontShowCardsCurrentTab();
this.dontShowIdentities = await this.stateService.getDontShowIdentitiesCurrentTab(); 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(); const defaultUriMatch = await this.stateService.getDefaultUriMatch();
this.defaultUriMatch = defaultUriMatch == null ? UriMatchType.Domain : defaultUriMatch; this.defaultUriMatch = defaultUriMatch == null ? UriMatchType.Domain : defaultUriMatch;
this.clearClipboard = await this.stateService.getClearClipboard(); this.clearClipboard = await this.stateService.getClearClipboard();
} }
async updateAddLoginNotification() { async updateAddLoginNotification() {
await this.stateService.setDisableAddLoginNotification(this.disableAddLoginNotification); await this.stateService.setDisableAddLoginNotification(this.disableAddLoginNotification);
} }
async updateChangedPasswordNotification() { async updateChangedPasswordNotification() {
await this.stateService.setDisableChangedPasswordNotification(this.disableChangedPasswordNotification); await this.stateService.setDisableChangedPasswordNotification(
} this.disableChangedPasswordNotification
);
}
async updateDisableContextMenuItem() { async updateDisableContextMenuItem() {
await this.stateService.setDisableContextMenuItem(this.disableContextMenuItem); await this.stateService.setDisableContextMenuItem(this.disableContextMenuItem);
this.messagingService.send('bgUpdateContextMenu'); this.messagingService.send("bgUpdateContextMenu");
} }
async updateAutoTotpCopy() { async updateAutoTotpCopy() {
await this.stateService.setDisableAutoTotpCopy(this.disableAutoTotpCopy); await this.stateService.setDisableAutoTotpCopy(this.disableAutoTotpCopy);
} }
async updateAutoFillOnPageLoad() { async updateAutoFillOnPageLoad() {
await this.stateService.setEnableAutoFillOnPageLoad(this.enableAutoFillOnPageLoad); await this.stateService.setEnableAutoFillOnPageLoad(this.enableAutoFillOnPageLoad);
} }
async updateAutoFillOnPageLoadDefault() { async updateAutoFillOnPageLoadDefault() {
await this.stateService.setAutoFillOnPageLoadDefault(this.autoFillOnPageLoadDefault); await this.stateService.setAutoFillOnPageLoadDefault(this.autoFillOnPageLoadDefault);
} }
async updateDisableFavicon() { async updateDisableFavicon() {
await this.stateService.setDisableFavicon(this.disableFavicon); await this.stateService.setDisableFavicon(this.disableFavicon);
} }
async updateDisableBadgeCounter() { async updateDisableBadgeCounter() {
await this.stateService.setDisableBadgeCounter(this.disableBadgeCounter); await this.stateService.setDisableBadgeCounter(this.disableBadgeCounter);
this.messagingService.send('bgUpdateContextMenu'); this.messagingService.send("bgUpdateContextMenu");
} }
async updateShowCards() { async updateShowCards() {
await this.stateService.setDontShowCardsCurrentTab(this.dontShowCards); await this.stateService.setDontShowCardsCurrentTab(this.dontShowCards);
} }
async updateShowIdentities() { async updateShowIdentities() {
await this.stateService.setDontShowIdentitiesCurrentTab(this.dontShowIdentities); await this.stateService.setDontShowIdentitiesCurrentTab(this.dontShowIdentities);
} }
async saveTheme() { async saveTheme() {
await this.stateService.setTheme(this.theme); await this.stateService.setTheme(this.theme);
window.setTimeout(() => window.location.reload(), 200); window.setTimeout(() => window.location.reload(), 200);
} }
async saveDefaultUriMatch() { async saveDefaultUriMatch() {
await this.stateService.setDefaultUriMatch(this.defaultUriMatch); await this.stateService.setDefaultUriMatch(this.defaultUriMatch);
} }
async saveClearClipboard() { async saveClearClipboard() {
await this.stateService.setClearClipboard(this.clearClipboard); await this.stateService.setClearClipboard(this.clearClipboard);
} }
} }

View File

@ -1,31 +1,36 @@
import { CurrencyPipe } from '@angular/common'; import { CurrencyPipe } from "@angular/common";
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PremiumComponent as BasePremiumComponent } from 'jslib-angular/components/premium.component'; import { PremiumComponent as BasePremiumComponent } from "jslib-angular/components/premium.component";
import { StateService } from '../../services/abstractions/state.service'; import { StateService } from "../../services/abstractions/state.service";
@Component({ @Component({
selector: 'app-premium', selector: "app-premium",
templateUrl: 'premium.component.html', templateUrl: "premium.component.html",
}) })
export class PremiumComponent extends BasePremiumComponent { export class PremiumComponent extends BasePremiumComponent {
priceString: string; priceString: string;
constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService, constructor(
apiService: ApiService, logService: LogService, stateService: StateService, i18nService: I18nService,
private currencyPipe: CurrencyPipe) { platformUtilsService: PlatformUtilsService,
super(i18nService, platformUtilsService, apiService, logService, stateService); 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. // Support old price string. Can be removed in future once all translations are properly updated.
const thePrice = this.currencyPipe.transform(this.price, '$'); const thePrice = this.currencyPipe.transform(this.price, "$");
this.priceString = i18nService.t('premiumPrice', thePrice); this.priceString = i18nService.t("premiumPrice", thePrice);
if (this.priceString.indexOf('%price%') > -1) { if (this.priceString.indexOf("%price%") > -1) {
this.priceString = this.priceString.replace('%price%', thePrice); this.priceString = this.priceString.replace("%price%", thePrice);
}
} }
}
} }

View File

@ -1,364 +1,410 @@
import { import { Component, ElementRef, OnInit, ViewChild } from "@angular/core";
Component, import { FormControl } from "@angular/forms";
ElementRef, import { Router } from "@angular/router";
OnInit, import Swal from "sweetalert2/src/sweetalert2.js";
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 { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { PopupUtilsService } from '../services/popup-utils.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 { StateService } from "../../services/abstractions/state.service";
import { SetPinComponent } from '../components/set-pin.component'; import { SetPinComponent } from "../components/set-pin.component";
const RateUrls = { const RateUrls = {
[DeviceType.ChromeExtension]: [DeviceType.ChromeExtension]:
'https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews', "https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews",
[DeviceType.FirefoxExtension]: [DeviceType.FirefoxExtension]:
'https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/#reviews', "https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/#reviews",
[DeviceType.OperaExtension]: [DeviceType.OperaExtension]:
'https://addons.opera.com/en/extensions/details/bitwarden-free-password-manager/#feedback-container', "https://addons.opera.com/en/extensions/details/bitwarden-free-password-manager/#feedback-container",
[DeviceType.EdgeExtension]: [DeviceType.EdgeExtension]:
'https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh', "https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh",
[DeviceType.VivaldiExtension]: [DeviceType.VivaldiExtension]:
'https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews', "https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews",
[DeviceType.SafariExtension]: [DeviceType.SafariExtension]: "https://apps.apple.com/app/bitwarden/id1352778147",
'https://apps.apple.com/app/bitwarden/id1352778147',
}; };
@Component({ @Component({
selector: 'app-settings', selector: "app-settings",
templateUrl: 'settings.component.html', templateUrl: "settings.component.html",
}) })
export class SettingsComponent implements OnInit { export class SettingsComponent implements OnInit {
@ViewChild('vaultTimeoutActionSelect', { read: ElementRef, static: true }) vaultTimeoutActionSelectRef: ElementRef; @ViewChild("vaultTimeoutActionSelect", { read: ElementRef, static: true })
vaultTimeouts: any[]; vaultTimeoutActionSelectRef: ElementRef;
vaultTimeoutActions: any[]; vaultTimeouts: any[];
vaultTimeoutAction: string; vaultTimeoutActions: any[];
pin: boolean = null; vaultTimeoutAction: string;
supportsBiometric: boolean; pin: boolean = null;
biometric: boolean = false; supportsBiometric: boolean;
disableAutoBiometricsPrompt = true; biometric: boolean = false;
previousVaultTimeout: number = null; disableAutoBiometricsPrompt = true;
showChangeMasterPass = true; previousVaultTimeout: number = null;
showChangeMasterPass = true;
vaultTimeout: FormControl = new FormControl(null); vaultTimeout: FormControl = new FormControl(null);
constructor(private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, constructor(
private vaultTimeoutService: VaultTimeoutService, private platformUtilsService: PlatformUtilsService,
public messagingService: MessagingService, private router: Router, private i18nService: I18nService,
private environmentService: EnvironmentService, private cryptoService: CryptoService, private vaultTimeoutService: VaultTimeoutService,
private stateService: StateService, private popupUtilsService: PopupUtilsService, public messagingService: MessagingService,
private modalService: ModalService, private router: Router,
private keyConnectorService: KeyConnectorService) { 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() { this.vaultTimeouts.push({ name: this.i18nService.t("onRestart"), value: -1 });
const showOnLocked = !this.platformUtilsService.isFirefox() && !this.platformUtilsService.isSafari(); this.vaultTimeouts.push({ name: this.i18nService.t("never"), value: null });
this.vaultTimeouts = [ this.vaultTimeoutActions = [
{ name: this.i18nService.t('immediately'), value: 0 }, { name: this.i18nService.t("lock"), value: "lock" },
{ name: this.i18nService.t('oneMinute'), value: 1 }, { name: this.i18nService.t("logOut"), value: "logOut" },
{ 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) { let timeout = await this.vaultTimeoutService.getVaultTimeout();
this.vaultTimeouts.push({ name: this.i18nService.t('onLocked'), value: -2 }); 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 }); const action = await this.stateService.getVaultTimeoutAction();
this.vaultTimeouts.push({ name: this.i18nService.t('never'), value: null }); this.vaultTimeoutAction = action == null ? "lock" : action;
this.vaultTimeoutActions = [ const pinSet = await this.vaultTimeoutService.isPinLockSet();
{ name: this.i18nService.t('lock'), value: 'lock' }, this.pin = pinSet[0] || pinSet[1];
{ name: this.i18nService.t('logOut'), value: 'logOut' },
];
let timeout = await this.vaultTimeoutService.getVaultTimeout(); this.supportsBiometric = await this.platformUtilsService.supportsBiometric();
if (timeout != null) { this.biometric = await this.vaultTimeoutService.isBiometricLockSet();
if (timeout === -2 && !showOnLocked) { this.disableAutoBiometricsPrompt =
timeout = -1; (await this.stateService.getDisableAutoBiometricsPrompt()) ?? true;
} this.showChangeMasterPass = !(await this.keyConnectorService.getUsesKeyConnector());
this.vaultTimeout.setValue(timeout); }
}
this.previousVaultTimeout = this.vaultTimeout.value; async saveVaultTimeout(newValue: number) {
this.vaultTimeout.valueChanges.subscribe(value => { if (newValue == null) {
this.saveVaultTimeout(value); 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;
}
}); });
return;
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();
} }
async saveVaultTimeout(newValue: number) { if (!this.vaultTimeout.valid) {
if (newValue == null) { this.platformUtilsService.showToast("error", null, this.i18nService.t("vaultTimeoutToLarge"));
const confirmed = await this.platformUtilsService.showDialog( return;
this.i18nService.t('neverLockWarning'), null, }
this.i18nService.t('yes'), this.i18nService.t('cancel'), 'warning');
if (!confirmed) { this.vaultTimeoutAction = newValue;
this.vaultTimeout.setValue(this.previousVaultTimeout); await this.vaultTimeoutService.setVaultTimeoutOptions(
return; 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: '<i class="swal-custom-icon fa fa-info-circle text-info"></i>',
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) => {
if (!this.vaultTimeout.valid) { // Handle connection errors
this.platformUtilsService.showToast('error', null, this.i18nService.t('vaultTimeoutToLarge')); this.biometric = false;
return; }),
} ]);
} else {
this.previousVaultTimeout = this.vaultTimeout.value; await this.stateService.setBiometricUnlock(null);
await this.stateService.setBiometricLocked(false);
await this.vaultTimeoutService.setVaultTimeoutOptions(this.vaultTimeout.value, this.vaultTimeoutAction);
if (this.previousVaultTimeout == null) {
this.messagingService.send('bgReseedStorage');
}
} }
}
async saveVaultTimeoutAction(newValue: string) { async updateAutoBiometricsPrompt() {
if (newValue === 'logOut') { await this.stateService.setDisableAutoBiometricsPrompt(this.disableAutoBiometricsPrompt);
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;
}
}
if (!this.vaultTimeout.valid) { async lock() {
this.platformUtilsService.showToast('error', null, this.i18nService.t('vaultTimeoutToLarge')); await this.vaultTimeoutService.lock(true);
return; }
}
this.vaultTimeoutAction = newValue; async logOut() {
await this.vaultTimeoutService.setVaultTimeoutOptions(this.vaultTimeout.value, this.vaultTimeoutAction); 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() { async changePassword() {
if (this.pin) { const confirmed = await this.platformUtilsService.showDialog(
const ref = this.modalService.open(SetPinComponent, { allowMultipleModals: true }); this.i18nService.t("changeMasterPasswordConfirmation"),
this.i18nService.t("changeMasterPassword"),
if (ref == null) { this.i18nService.t("yes"),
this.pin = false; this.i18nService.t("cancel")
return; );
} if (confirmed) {
BrowserApi.createNewTab("https://help.bitwarden.com/article/change-your-master-password/");
this.pin = await ref.onClosedPromise();
} else {
await this.cryptoService.clearPinProtectedKey();
await this.vaultTimeoutService.clear();
}
} }
}
async updateBiometric() { async twoStep() {
if (this.biometric && this.supportsBiometric) { const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("twoStepLoginConfirmation"),
let granted; this.i18nService.t("twoStepLogin"),
try { this.i18nService.t("yes"),
granted = await BrowserApi.requestPermission({ permissions: ['nativeMessaging'] }); this.i18nService.t("cancel")
} catch (e) { );
// tslint:disable-next-line if (confirmed) {
console.error(e); BrowserApi.createNewTab("https://help.bitwarden.com/article/setup-two-step-login/");
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: '<i class="swal-custom-icon fa fa-info-circle text-info"></i>',
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 updateAutoBiometricsPrompt() { async share() {
await this.stateService.setDisableAutoBiometricsPrompt(this.disableAutoBiometricsPrompt); 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() { async webVault() {
await this.vaultTimeoutService.lock(true); 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 =
`<p class="text-center"><i class="fa fa-shield fa-3x" aria-hidden="true"></i></p>
<p class="text-center"><b>Bitwarden</b><br>&copy; Bitwarden Inc. 2015-` +
year +
`</p>`;
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() { rate() {
const confirmed = await this.platformUtilsService.showDialog( const deviceType = this.platformUtilsService.getDevice();
this.i18nService.t('logOutConfirmation'), this.i18nService.t('logOut'), BrowserApi.createNewTab((RateUrls as any)[deviceType]);
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 = `<p class="text-center"><i class="fa fa-shield fa-3x" aria-hidden="true"></i></p>
<p class="text-center"><b>Bitwarden</b><br>&copy; Bitwarden Inc. 2015-` + year + `</p>`;
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]);
}
} }

View File

@ -1,190 +1,225 @@
import { Location } from '@angular/common'; import { Location } from "@angular/common";
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { import { ActivatedRoute, Router } from "@angular/router";
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 { AuditService } from "jslib-common/abstractions/audit.service";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from 'jslib-common/abstractions/collection.service'; import { CollectionService } from "jslib-common/abstractions/collection.service";
import { EventService } from 'jslib-common/abstractions/event.service'; import { EventService } from "jslib-common/abstractions/event.service";
import { FolderService } from 'jslib-common/abstractions/folder.service'; import { FolderService } from "jslib-common/abstractions/folder.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { OrganizationService } from 'jslib-common/abstractions/organization.service'; import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service'; import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { PolicyService } from "jslib-common/abstractions/policy.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 { 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({ @Component({
selector: 'app-vault-add-edit', selector: "app-vault-add-edit",
templateUrl: 'add-edit.component.html', templateUrl: "add-edit.component.html",
}) })
export class AddEditComponent extends BaseAddEditComponent { export class AddEditComponent extends BaseAddEditComponent {
currentUris: string[]; currentUris: string[];
showAttachments = true; showAttachments = true;
openAttachmentsInPopup: boolean; openAttachmentsInPopup: boolean;
showAutoFillOnPageLoadOptions: boolean; showAutoFillOnPageLoadOptions: boolean;
constructor(cipherService: CipherService, folderService: FolderService, constructor(
i18nService: I18nService, platformUtilsService: PlatformUtilsService, cipherService: CipherService,
auditService: AuditService, stateService: StateService, collectionService: CollectionService, folderService: FolderService,
messagingService: MessagingService, private route: ActivatedRoute, i18nService: I18nService,
private router: Router, private location: Location, platformUtilsService: PlatformUtilsService,
eventService: EventService, policyService: PolicyService, auditService: AuditService,
private popupUtilsService: PopupUtilsService, organizationService: OrganizationService, stateService: StateService,
passwordRepromptService: PasswordRepromptService, logService: LogService) { collectionService: CollectionService,
super(cipherService, folderService, i18nService, platformUtilsService, messagingService: MessagingService,
auditService, stateService, collectionService, messagingService, private route: ActivatedRoute,
eventService, policyService, logService, passwordRepromptService, organizationService); 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() { async ngOnInit() {
await super.ngOnInit(); await super.ngOnInit();
this.route.queryParams.pipe(first()).subscribe(async params => { this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (params.cipherId) { if (params.cipherId) {
this.cipherId = params.cipherId; this.cipherId = params.cipherId;
} }
if (params.folderId) { if (params.folderId) {
this.folderId = params.folderId; this.folderId = params.folderId;
} }
if (params.collectionId) { if (params.collectionId) {
const collection = this.writeableCollections.find(c => c.id === params.collectionId); const collection = this.writeableCollections.find((c) => c.id === params.collectionId);
if (collection != null) { if (collection != null) {
this.collectionIds = [collection.id]; this.collectionIds = [collection.id];
this.organizationId = collection.organizationId; 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);
} }
}
if (params.type) {
const type = parseInt(params.type, null);
this.type = type;
}
this.editMode = !params.cipherId;
window.setTimeout(() => { if (params.cloneMode != null) {
if (!this.editMode) { this.cloneMode = params.cloneMode === "true";
if (this.cipher.name != null && this.cipher.name !== '') { }
document.getElementById('loginUsername').focus(); await this.load();
} else {
document.getElementById('name').focus();
}
}
}, 200);
}
async load() { if (!this.editMode || this.cloneMode) {
await super.load(); if (
this.showAutoFillOnPageLoadOptions = this.cipher.type === CipherType.Login && !this.popupUtilsService.inPopout(window) &&
await this.stateService.getEnableAutoFillOnPageLoad(); params.name &&
} (this.cipher.name == null || this.cipher.name === "")
) {
async submit(): Promise<boolean> { this.cipher.name = params.name;
if (await super.submit()) {
if (this.cloneMode) {
this.router.navigate(['/tabs/vault']);
} else {
this.location.back();
}
return true;
} }
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() { window.setTimeout(() => {
super.attachments(); if (!this.editMode) {
if (this.cipher.name != null && this.cipher.name !== "") {
if (this.openAttachmentsInPopup) { document.getElementById("loginUsername").focus();
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 { } 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() { async submit(): Promise<boolean> {
super.editCollections(); if (await super.submit()) {
if (this.cipher.organizationId != null) { if (this.cloneMode) {
this.router.navigate(['/collections'], { queryParams: { cipherId: this.cipher.id } }); this.router.navigate(["/tabs/vault"]);
} } else {
}
cancel() {
super.cancel();
this.location.back(); this.location.back();
}
return true;
} }
async generatePassword(): Promise<boolean> { return false;
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<boolean> { attachments() {
const confirmed = await super.delete(); super.attachments();
if (confirmed) {
this.router.navigate(['/tabs/vault']);
}
return confirmed;
}
toggleUriInput(uri: LoginUriView) { if (this.openAttachmentsInPopup) {
const u = (uri as any); const destinationUrl = this.router
u.showCurrentUris = !u.showCurrentUris; .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 { editCollections() {
return (!this.editMode || this.cloneMode) && this.ownershipOptions super.editCollections();
&& (this.ownershipOptions.length > 1 || !this.allowPersonal); if (this.cipher.organizationId != null) {
this.router.navigate(["/collections"], { queryParams: { cipherId: this.cipher.id } });
} }
}
cancel() {
super.cancel();
this.location.back();
}
async generatePassword(): Promise<boolean> {
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<boolean> {
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)
);
}
} }

View File

@ -1,48 +1,63 @@
import { Location } from '@angular/common'; import { Location } from "@angular/common";
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { first } from 'rxjs/operators'; import { first } from "rxjs/operators";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { AttachmentsComponent as BaseAttachmentsComponent } from 'jslib-angular/components/attachments.component'; import { AttachmentsComponent as BaseAttachmentsComponent } from "jslib-angular/components/attachments.component";
import { StateService } from '../../services/abstractions/state.service'; import { StateService } from "../../services/abstractions/state.service";
@Component({ @Component({
selector: 'app-vault-attachments', selector: "app-vault-attachments",
templateUrl: 'attachments.component.html', templateUrl: "attachments.component.html",
}) })
export class AttachmentsComponent extends BaseAttachmentsComponent { export class AttachmentsComponent extends BaseAttachmentsComponent {
openedAttachmentsInPopup: boolean; openedAttachmentsInPopup: boolean;
constructor(cipherService: CipherService, i18nService: I18nService, constructor(
cryptoService: CryptoService, platformUtilsService: PlatformUtilsService, cipherService: CipherService,
apiService: ApiService, private location: Location, i18nService: I18nService,
private route: ActivatedRoute, stateService: StateService, logService: LogService) { cryptoService: CryptoService,
super(cipherService, i18nService, cryptoService, platformUtilsService, platformUtilsService: PlatformUtilsService,
apiService, window, logService, stateService); apiService: ApiService,
} private location: Location,
private route: ActivatedRoute,
stateService: StateService,
logService: LogService
) {
super(
cipherService,
i18nService,
cryptoService,
platformUtilsService,
apiService,
window,
logService,
stateService
);
}
async ngOnInit() { async ngOnInit() {
this.route.queryParams.pipe(first()).subscribe(async params => { this.route.queryParams.pipe(first()).subscribe(async (params) => {
this.cipherId = params.cipherId; this.cipherId = params.cipherId;
await this.init(); await this.init();
}); });
this.openedAttachmentsInPopup = history.length === 1; this.openedAttachmentsInPopup = history.length === 1;
} }
back() { back() {
this.location.back(); this.location.back();
} }
close() { close() {
window.close(); window.close();
} }
} }

View File

@ -1,235 +1,247 @@
import { Location } from '@angular/common'; import { Location } from "@angular/common";
import { import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
ChangeDetectorRef, import { ActivatedRoute, Router } from "@angular/router";
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 { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from 'jslib-common/abstractions/collection.service'; import { CollectionService } from "jslib-common/abstractions/collection.service";
import { FolderService } from 'jslib-common/abstractions/folder.service'; import { FolderService } from "jslib-common/abstractions/folder.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SearchService } from 'jslib-common/abstractions/search.service'; import { SearchService } from "jslib-common/abstractions/search.service";
import { StateService } from '../../services/abstractions/state.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 { CipherView } from "jslib-common/models/view/cipherView";
import { CollectionView } from 'jslib-common/models/view/collectionView'; import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from 'jslib-common/models/view/folderView'; 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 { BrowserComponentState } from "../../models/browserComponentState";
import { PopupUtilsService } from '../services/popup-utils.service'; import { PopupUtilsService } from "../services/popup-utils.service";
const ComponentId = 'CiphersComponent'; const ComponentId = "CiphersComponent";
@Component({ @Component({
selector: 'app-vault-ciphers', selector: "app-vault-ciphers",
templateUrl: 'ciphers.component.html', templateUrl: "ciphers.component.html",
}) })
export class CiphersComponent extends BaseCiphersComponent implements OnInit, OnDestroy { export class CiphersComponent extends BaseCiphersComponent implements OnInit, OnDestroy {
groupingTitle: string; groupingTitle: string;
state: BrowserComponentState; state: BrowserComponentState;
folderId: string = null; folderId: string = null;
collectionId: string = null; collectionId: string = null;
type: CipherType = null; type: CipherType = null;
nestedFolders: TreeNode<FolderView>[]; nestedFolders: TreeNode<FolderView>[];
nestedCollections: TreeNode<CollectionView>[]; nestedCollections: TreeNode<CollectionView>[];
searchTypeSearch = false; searchTypeSearch = false;
private selectedTimeout: number; private selectedTimeout: number;
private preventSelected = false; private preventSelected = false;
private applySavedState = true; private applySavedState = true;
private scrollingContainer = 'cdk-virtual-scroll-viewport'; private scrollingContainer = "cdk-virtual-scroll-viewport";
constructor(searchService: SearchService, private route: ActivatedRoute, constructor(
private router: Router, private location: Location, searchService: SearchService,
private ngZone: NgZone, private broadcasterService: BroadcasterService, private route: ActivatedRoute,
private changeDetectorRef: ChangeDetectorRef, private stateService: StateService, private router: Router,
private popupUtils: PopupUtilsService, private i18nService: I18nService, private location: Location,
private folderService: FolderService, private collectionService: CollectionService, private ngZone: NgZone,
private platformUtilsService: PlatformUtilsService, private cipherService: CipherService) { private broadcasterService: BroadcasterService,
super(searchService); private changeDetectorRef: ChangeDetectorRef,
this.applySavedState = (window as any).previousPopupUrl != null && private stateService: StateService,
!(window as any).previousPopupUrl.startsWith('/ciphers'); 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() { async ngOnInit() {
this.searchTypeSearch = !this.platformUtilsService.isSafari(); this.searchTypeSearch = !this.platformUtilsService.isSafari();
this.route.queryParams.pipe(first()).subscribe(async params => { this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (this.applySavedState) { if (this.applySavedState) {
this.state = await this.stateService.getBrowserCipherComponentState(); this.state = await this.stateService.getBrowserCipherComponentState();
if (this.state?.searchText) { if (this.state?.searchText) {
this.searchText = 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);
} }
break;
if (params.deleted) { default:
this.groupingTitle = this.i18nService.t('trash'); break;
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;
} }
if (this.selectedTimeout != null) { this.changeDetectorRef.detectChanges();
window.clearTimeout(this.selectedTimeout); });
} });
this.preventSelected = true; }
await this.cipherService.updateLastLaunchedDate(cipher.id);
BrowserApi.createNewTab(cipher.login.launchUri); ngOnDestroy() {
if (this.popupUtils.inPopup(window)) { this.saveState();
BrowserApi.closePopup(window); 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.selectedTimeout != null) {
if (this.deleted) { window.clearTimeout(this.selectedTimeout);
return false;
}
super.addCipher();
this.router.navigate(['/add-cipher'], {
queryParams: {
folderId: this.folderId,
type: this.type,
collectionId: this.collectionId,
},
});
} }
this.preventSelected = true;
await this.cipherService.updateLastLaunchedDate(cipher.id);
BrowserApi.createNewTab(cipher.login.launchUri);
if (this.popupUtils.inPopup(window)) {
BrowserApi.closePopup(window);
}
}
back() { addCipher() {
(window as any).routeDirection = 'b'; if (this.deleted) {
this.location.back(); return false;
} }
super.addCipher();
this.router.navigate(["/add-cipher"], {
queryParams: {
folderId: this.folderId,
type: this.type,
collectionId: this.collectionId,
},
});
}
showGroupings() { back() {
return !this.isSearching() && (window as any).routeDirection = "b";
((this.nestedFolders && this.nestedFolders.length) || this.location.back();
(this.nestedCollections && this.nestedCollections.length)); }
}
private async saveState() { showGroupings() {
this.state = { return (
scrollY: this.popupUtils.getContentScrollY(window, this.scrollingContainer), !this.isSearching() &&
searchText: this.searchText, ((this.nestedFolders && this.nestedFolders.length) ||
}; (this.nestedCollections && this.nestedCollections.length))
await this.stateService.setBrowserCipherComponentState(this.state); );
} }
private async saveState() {
this.state = {
scrollY: this.popupUtils.getContentScrollY(window, this.scrollingContainer),
searchText: this.searchText,
};
await this.stateService.setBrowserCipherComponentState(this.state);
}
} }

View File

@ -1,38 +1,44 @@
import { Location } from '@angular/common'; import { Location } from "@angular/common";
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { first } from 'rxjs/operators'; import { first } from "rxjs/operators";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from 'jslib-common/abstractions/collection.service'; import { CollectionService } from "jslib-common/abstractions/collection.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.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({ @Component({
selector: 'app-vault-collections', selector: "app-vault-collections",
templateUrl: 'collections.component.html', templateUrl: "collections.component.html",
}) })
export class CollectionsComponent extends BaseCollectionsComponent { export class CollectionsComponent extends BaseCollectionsComponent {
constructor(collectionService: CollectionService, platformUtilsService: PlatformUtilsService, constructor(
i18nService: I18nService, cipherService: CipherService, collectionService: CollectionService,
private route: ActivatedRoute, private location: Location, logService: LogService) { platformUtilsService: PlatformUtilsService,
super(collectionService, platformUtilsService, i18nService, cipherService, logService); i18nService: I18nService,
} cipherService: CipherService,
private route: ActivatedRoute,
private location: Location,
logService: LogService
) {
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
}
async ngOnInit() { async ngOnInit() {
this.onSavedCollections.subscribe(() => { this.onSavedCollections.subscribe(() => {
this.back(); this.back();
}); });
this.route.queryParams.pipe(first()).subscribe(async params => { this.route.queryParams.pipe(first()).subscribe(async (params) => {
this.cipherId = params.cipherId; this.cipherId = params.cipherId;
await this.load(); await this.load();
}); });
} }
back() { back() {
this.location.back(); this.location.back();
} }
} }

View File

@ -1,241 +1,249 @@
import { import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
ChangeDetectorRef, import { Router } from "@angular/router";
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 { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
import { CipherType } from 'jslib-common/enums/cipherType'; 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 { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service'; import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SearchService } from 'jslib-common/abstractions/search.service'; import { SearchService } from "jslib-common/abstractions/search.service";
import { SyncService } from 'jslib-common/abstractions/sync.service'; import { SyncService } from "jslib-common/abstractions/sync.service";
import { AutofillService } from '../../services/abstractions/autofill.service'; import { AutofillService } from "../../services/abstractions/autofill.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 { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
const BroadcasterSubscriptionId = 'CurrentTabComponent'; const BroadcasterSubscriptionId = "CurrentTabComponent";
@Component({ @Component({
selector: 'app-current-tab', selector: "app-current-tab",
templateUrl: 'current-tab.component.html', templateUrl: "current-tab.component.html",
}) })
export class CurrentTabComponent implements OnInit, OnDestroy { export class CurrentTabComponent implements OnInit, OnDestroy {
pageDetails: any[] = []; pageDetails: any[] = [];
cardCiphers: CipherView[]; cardCiphers: CipherView[];
identityCiphers: CipherView[]; identityCiphers: CipherView[];
loginCiphers: CipherView[]; loginCiphers: CipherView[];
url: string; url: string;
hostname: string; hostname: string;
searchText: string; searchText: string;
inSidebar = false; inSidebar = false;
searchTypeSearch = false; searchTypeSearch = false;
loaded = false; loaded = false;
private totpCode: string; private totpCode: string;
private totpTimeout: number; private totpTimeout: number;
private loadedTimeout: number; private loadedTimeout: number;
private searchTimeout: number; private searchTimeout: number;
constructor(private platformUtilsService: PlatformUtilsService, private cipherService: CipherService, constructor(
private popupUtilsService: PopupUtilsService, private autofillService: AutofillService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private router: Router, private cipherService: CipherService,
private ngZone: NgZone, private broadcasterService: BroadcasterService, private popupUtilsService: PopupUtilsService,
private changeDetectorRef: ChangeDetectorRef, private syncService: SyncService, private autofillService: AutofillService,
private searchService: SearchService, private stateService: StateService, private i18nService: I18nService,
private passwordRepromptService: PasswordRepromptService) { 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() { window.setTimeout(() => {
this.searchTypeSearch = !this.platformUtilsService.isSafari(); document.getElementById("search").focus();
this.inSidebar = this.popupUtilsService.inSidebar(window); }, 100);
}
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { ngOnDestroy() {
this.ngZone.run(async () => { window.clearTimeout(this.loadedTimeout);
switch (message.command) { this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
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(); async refresh() {
}); await this.load();
}); }
if (!this.syncService.syncInProgress) { addCipher() {
await this.load(); 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 { } else {
this.loadedTimeout = window.setTimeout(async () => { // Slight delay to fix bug in Chromium browsers where popup closes without copying totp to clipboard
if (!this.loaded) { setTimeout(() => BrowserApi.closePopup(window), 50);
await this.load();
}
}, 5000);
} }
}
} catch {
this.ngZone.run(() => {
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
this.changeDetectorRef.detectChanges();
});
}
}
window.setTimeout(() => { searchVault() {
document.getElementById('search').focus(); if (this.searchTimeout != null) {
}, 100); 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() { this.hostname = Utils.getHostname(this.url);
window.clearTimeout(this.loadedTimeout); this.pageDetails = [];
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); 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() { const ciphers = await this.cipherService.getAllDecryptedForUrl(
await this.load(); this.url,
} otherTypes.length > 0 ? otherTypes : null
);
addCipher() { this.loginCiphers = [];
this.router.navigate(['/add-cipher'], { queryParams: { name: this.hostname, uri: this.url } }); this.cardCiphers = [];
} this.identityCiphers = [];
viewCipher(cipher: CipherView) { ciphers.forEach((c) => {
this.router.navigate(['/view-cipher'], { queryParams: { cipherId: cipher.id } }); 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) { this.loginCiphers = this.loginCiphers.sort((a, b) =>
if (cipher.reprompt !== CipherRepromptType.None && !await this.passwordRepromptService.showPasswordPrompt()) { this.cipherService.sortCiphersByLastUsedThenName(a, b)
return; );
} this.loaded = true;
}
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;
}
} }

View File

@ -1,355 +1,369 @@
import { Location } from '@angular/common'; import { Location } from "@angular/common";
import { import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
ChangeDetectorRef, import { ActivatedRoute, Router } from "@angular/router";
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 { CipherView } from "jslib-common/models/view/cipherView";
import { CollectionView } from 'jslib-common/models/view/collectionView'; import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from 'jslib-common/models/view/folderView'; import { FolderView } from "jslib-common/models/view/folderView";
import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from 'jslib-common/abstractions/collection.service'; import { CollectionService } from "jslib-common/abstractions/collection.service";
import { FolderService } from 'jslib-common/abstractions/folder.service'; import { FolderService } from "jslib-common/abstractions/folder.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SearchService } from 'jslib-common/abstractions/search.service'; import { SearchService } from "jslib-common/abstractions/search.service";
import { SyncService } from 'jslib-common/abstractions/sync.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 { StateService } from "../../services/abstractions/state.service";
import { PopupUtilsService } from '../services/popup-utils.service'; import { PopupUtilsService } from "../services/popup-utils.service";
const ComponentId = 'GroupingsComponent'; const ComponentId = "GroupingsComponent";
const ScopeStateId = ComponentId + 'Scope'; const ScopeStateId = ComponentId + "Scope";
@Component({ @Component({
selector: 'app-vault-groupings', selector: "app-vault-groupings",
templateUrl: 'groupings.component.html', templateUrl: "groupings.component.html",
}) })
export class GroupingsComponent extends BaseGroupingsComponent implements OnInit, OnDestroy { 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 { get folderCount(): number {
return this.noFolderCiphers != null && this.noFolderCiphers.length < this.noFolderListSize && return this.nestedFolders.length - (this.showNoFolderCiphers ? 0 : 1);
this.collections.length === 0; }
} ciphers: CipherView[];
favoriteCiphers: CipherView[];
noFolderCiphers: CipherView[];
folderCounts = new Map<string, number>();
collectionCounts = new Map<string, number>();
typeCounts = new Map<CipherType, number>();
searchText: string;
state: any;
scopeState: any;
showLeftHeader = true;
searchPending = false;
searchTypeSearch = false;
deletedCount = 0;
get folderCount(): number { private loadedTimeout: number;
return this.nestedFolders.length - (this.showNoFolderCiphers ? 0 : 1); private selectedTimeout: number;
} private preventSelected = false;
ciphers: CipherView[]; private noFolderListSize = 100;
favoriteCiphers: CipherView[]; private searchTimeout: any = null;
noFolderCiphers: CipherView[]; private hasSearched = false;
folderCounts = new Map<string, number>(); private hasLoadedAllCiphers = false;
collectionCounts = new Map<string, number>(); private allCiphers: CipherView[] = null;
typeCounts = new Map<CipherType, number>();
searchText: string;
state: any;
scopeState: any;
showLeftHeader = true;
searchPending = false;
searchTypeSearch = false;
deletedCount = 0;
private loadedTimeout: number; constructor(
private selectedTimeout: number; collectionService: CollectionService,
private preventSelected = false; folderService: FolderService,
private noFolderListSize = 100; private cipherService: CipherService,
private searchTimeout: any = null; private router: Router,
private hasSearched = false; private ngZone: NgZone,
private hasLoadedAllCiphers = false; private broadcasterService: BroadcasterService,
private allCiphers: CipherView[] = null; 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, async ngOnInit() {
private cipherService: CipherService, private router: Router, this.searchTypeSearch = !this.platformUtilsService.isSafari();
private ngZone: NgZone, private broadcasterService: BroadcasterService, this.showLeftHeader = !(
private changeDetectorRef: ChangeDetectorRef, private route: ActivatedRoute, this.popupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()
baseStateService: StateService, private popupUtils: PopupUtilsService, );
private syncService: SyncService, private platformUtilsService: PlatformUtilsService, await this.browserStateService.setBrowserCipherComponentState(null);
private searchService: SearchService, private location: Location,
private browserStateService: StateService) {
super(collectionService, folderService, baseStateService);
this.noFolderListSize = 100;
}
async ngOnInit() { this.broadcasterService.subscribe(ComponentId, (message: any) => {
this.searchTypeSearch = !this.platformUtilsService.isSafari(); this.ngZone.run(async () => {
this.showLeftHeader = !(this.popupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()); switch (message.command) {
await this.browserStateService.setBrowserCipherComponentState(null); case "syncCompleted":
window.setTimeout(() => {
this.broadcasterService.subscribe(ComponentId, (message: any) => { this.load();
this.ngZone.run(async () => { }, 500);
switch (message.command) { break;
case 'syncCompleted': default:
window.setTimeout(() => { break;
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);
} }
if (this.selectedTimeout != null) {
window.clearTimeout(this.selectedTimeout); this.changeDetectorRef.detectChanges();
} });
this.saveState(); });
this.broadcasterService.unsubscribe(ComponentId);
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() { super.loaded = true;
await super.load(false); }
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<string, number>();
const collectionCounts = new Map<string, number>();
const typeCounts = new Map<CipherType, number>();
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(); await this.loadCiphers();
if (this.showNoFolderCiphers && this.nestedFolders.length > 0) { } else {
// Remove "No Folder" from folder listing this.ciphers = await this.searchService.searchCiphers(
this.nestedFolders = this.nestedFolders.slice(0, this.nestedFolders.length - 1); 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() { if (this.selectedTimeout != null) {
this.allCiphers = await this.cipherService.getAllDecrypted(); window.clearTimeout(this.selectedTimeout);
if (!this.hasLoadedAllCiphers) { }
this.hasLoadedAllCiphers = !this.searchService.isSearchable(this.searchText); this.preventSelected = true;
} await this.cipherService.updateLastLaunchedDate(cipher.id);
this.deletedCount = this.allCiphers.filter(c => c.isDeleted).length; BrowserApi.createNewTab(cipher.login.launchUri);
await this.search(null); if (this.popupUtils.inPopup(window)) {
let favoriteCiphers: CipherView[] = null; BrowserApi.closePopup(window);
let noFolderCiphers: CipherView[] = null; }
const folderCounts = new Map<string, number>(); }
const collectionCounts = new Map<string, number>();
const typeCounts = new Map<CipherType, number>();
this.ciphers.forEach(c => { async addCipher() {
if (c.isDeleted) { this.router.navigate(["/add-cipher"]);
return; }
}
if (c.favorite) {
if (favoriteCiphers == null) {
favoriteCiphers = [];
}
favoriteCiphers.push(c);
}
if (c.folderId == null) { showSearching() {
if (noFolderCiphers == null) { return (
noFolderCiphers = []; this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText))
} );
noFolderCiphers.push(c); }
}
if (typeCounts.has(c.type)) { closeOnEsc(e: KeyboardEvent) {
typeCounts.set(c.type, typeCounts.get(c.type) + 1); // If input not empty, use browser default behavior of clearing input instead
} else { if (e.key === "Escape" && (this.searchText == null || this.searchText === "")) {
typeCounts.set(c.type, 1); BrowserApi.closePopup(window);
} }
}
if (folderCounts.has(c.folderId)) { private async saveState() {
folderCounts.set(c.folderId, folderCounts.get(c.folderId) + 1); this.state = {
} else { scrollY: this.popupUtils.getContentScrollY(window),
folderCounts.set(c.folderId, 1); 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) { private async restoreState(): Promise<boolean> {
c.collectionIds.forEach(colId => { this.scopeState = await this.browserStateService.getBrowserGroupingComponentState();
if (collectionCounts.has(colId)) { if (this.scopeState == null) {
collectionCounts.set(colId, collectionCounts.get(colId) + 1); return false;
} 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) { if (this.scopeState.favoriteCiphers != null) {
this.searchPending = false; this.favoriteCiphers = this.scopeState.favoriteCiphers;
if (this.searchTimeout != null) { }
clearTimeout(this.searchTimeout); if (this.scopeState.noFolderCiphers != null) {
} this.noFolderCiphers = this.scopeState.noFolderCiphers;
const filterDeleted = (c: CipherView) => !c.isDeleted; }
if (timeout == null) { if (this.scopeState.ciphers != null) {
this.hasSearched = this.searchService.isSearchable(this.searchText); this.ciphers = this.scopeState.ciphers;
this.ciphers = await this.searchService.searchCiphers(this.searchText, filterDeleted, this.allCiphers); }
return; if (this.scopeState.collectionCounts != null) {
} this.collectionCounts = this.scopeState.collectionCounts;
this.searchPending = true; }
this.searchTimeout = setTimeout(async () => { if (this.scopeState.folderCounts != null) {
this.hasSearched = this.searchService.isSearchable(this.searchText); this.folderCounts = this.scopeState.folderCounts;
if (!this.hasLoadedAllCiphers && !this.hasSearched) { }
await this.loadCiphers(); if (this.scopeState.typeCounts != null) {
} else { this.typeCounts = this.scopeState.typeCounts;
this.ciphers = await this.searchService.searchCiphers(this.searchText, filterDeleted, this.allCiphers); }
} if (this.scopeState.folders != null) {
this.searchPending = false; this.folders = this.scopeState.folders;
}, timeout); }
if (this.scopeState.collections != null) {
this.collections = this.scopeState.collections;
}
if (this.scopeState.deletedCiphers != null) {
this.deletedCount = this.scopeState.deletedCount;
} }
async selectType(type: CipherType) { return true;
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<boolean> {
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;
}
} }

View File

@ -1,51 +1,63 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { import { ActivatedRoute, Router } from "@angular/router";
ActivatedRoute,
Router,
} from '@angular/router';
import { first } from 'rxjs/operators'; import { first } from "rxjs/operators";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from 'jslib-common/abstractions/collection.service'; import { CollectionService } from "jslib-common/abstractions/collection.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { OrganizationService } from 'jslib-common/abstractions/organization.service'; import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.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({ @Component({
selector: 'app-vault-share', selector: "app-vault-share",
templateUrl: 'share.component.html', templateUrl: "share.component.html",
}) })
export class ShareComponent extends BaseShareComponent { export class ShareComponent extends BaseShareComponent {
constructor(collectionService: CollectionService, platformUtilsService: PlatformUtilsService, constructor(
i18nService: I18nService, logService: LogService, collectionService: CollectionService,
cipherService: CipherService, private route: ActivatedRoute, platformUtilsService: PlatformUtilsService,
private router: Router, organizationService: OrganizationService) { i18nService: I18nService,
super(collectionService, platformUtilsService, i18nService, cipherService, logService: LogService,
logService, organizationService); cipherService: CipherService,
} private route: ActivatedRoute,
private router: Router,
organizationService: OrganizationService
) {
super(
collectionService,
platformUtilsService,
i18nService,
cipherService,
logService,
organizationService
);
}
async ngOnInit() { async ngOnInit() {
this.onSharedCipher.subscribe(() => { this.onSharedCipher.subscribe(() => {
this.router.navigate(['view-cipher', { cipherId: this.cipherId }]); this.router.navigate(["view-cipher", { cipherId: this.cipherId }]);
}); });
this.route.queryParams.pipe(first()).subscribe(async params => { this.route.queryParams.pipe(first()).subscribe(async (params) => {
this.cipherId = params.cipherId; this.cipherId = params.cipherId;
await this.load(); await this.load();
}); });
} }
async submit(): Promise<boolean> { async submit(): Promise<boolean> {
const success = await super.submit(); const success = await super.submit();
if (success) { if (success) {
this.cancel(); this.cancel();
}
return success;
} }
return success;
}
cancel() { cancel() {
this.router.navigate(['/view-cipher'], { replaceUrl: true, queryParams: { cipherId: this.cipher.id } }); this.router.navigate(["/view-cipher"], {
} replaceUrl: true,
queryParams: { cipherId: this.cipher.id },
});
}
} }

View File

@ -1,273 +1,297 @@
import { Location } from '@angular/common'; import { Location } from "@angular/common";
import { import { ChangeDetectorRef, Component, NgZone } from "@angular/core";
ChangeDetectorRef, import { ActivatedRoute, Router } from "@angular/router";
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 { ApiService } from "jslib-common/abstractions/api.service";
import { AuditService } from 'jslib-common/abstractions/audit.service'; import { AuditService } from "jslib-common/abstractions/audit.service";
import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EventService } from 'jslib-common/abstractions/event.service'; import { EventService } from "jslib-common/abstractions/event.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service'; import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TokenService } from 'jslib-common/abstractions/token.service'; import { TokenService } from "jslib-common/abstractions/token.service";
import { TotpService } from 'jslib-common/abstractions/totp.service'; import { TotpService } from "jslib-common/abstractions/totp.service";
import { Cipher } from 'jslib-common/models/domain/cipher'; import { Cipher } from "jslib-common/models/domain/cipher";
import { LoginUriView } from 'jslib-common/models/view/loginUriView'; 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 { AutofillService } from "../../services/abstractions/autofill.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";
const BroadcasterSubscriptionId = 'ChildViewComponent'; const BroadcasterSubscriptionId = "ChildViewComponent";
@Component({ @Component({
selector: 'app-vault-view', selector: "app-vault-view",
templateUrl: 'view.component.html', templateUrl: "view.component.html",
}) })
export class ViewComponent extends BaseViewComponent { export class ViewComponent extends BaseViewComponent {
showAttachments = true; showAttachments = true;
pageDetails: any[] = []; pageDetails: any[] = [];
tab: any; tab: any;
loadPageDetailsTimeout: number; loadPageDetailsTimeout: number;
inPopout = false; inPopout = false;
cipherType = CipherType; cipherType = CipherType;
constructor(cipherService: CipherService, totpService: TotpService, constructor(
tokenService: TokenService, i18nService: I18nService, cipherService: CipherService,
cryptoService: CryptoService, platformUtilsService: PlatformUtilsService, totpService: TotpService,
auditService: AuditService, private route: ActivatedRoute, tokenService: TokenService,
private router: Router, private location: Location, i18nService: I18nService,
broadcasterService: BroadcasterService, ngZone: NgZone, cryptoService: CryptoService,
changeDetectorRef: ChangeDetectorRef, stateService: StateService, platformUtilsService: PlatformUtilsService,
eventService: EventService, private autofillService: AutofillService, auditService: AuditService,
private messagingService: MessagingService, private popupUtilsService: PopupUtilsService, private route: ActivatedRoute,
apiService: ApiService, passwordRepromptService: PasswordRepromptService, private router: Router,
logService: LogService) { private location: Location,
super(cipherService, totpService, tokenService, i18nService, broadcasterService: BroadcasterService,
cryptoService, platformUtilsService, auditService, window, ngZone: NgZone,
broadcasterService, ngZone, changeDetectorRef, eventService, changeDetectorRef: ChangeDetectorRef,
apiService, passwordRepromptService, logService, stateService); 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() { ngOnInit() {
this.inPopout = this.popupUtilsService.inPopout(window); this.inPopout = this.popupUtilsService.inPopout(window);
this.route.queryParams.pipe(first()).subscribe(async params => { this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (params.cipherId) { if (params.cipherId) {
this.cipherId = params.cipherId; this.cipherId = params.cipherId;
} else { } else {
this.close(); 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,
});
} }
break;
await this.load(); case "tabChanged":
}); case "windowChanged":
if (this.loadPageDetailsTimeout != null) {
super.ngOnInit(); window.clearTimeout(this.loadPageDetailsTimeout);
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'));
} }
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() { this.router.navigate(["/edit-cipher"], { queryParams: { cipherId: this.cipher.id } });
if (!this.cipher.isDeleted) { return true;
return false; }
}
if (await super.restore()) { async clone() {
this.close(); if (this.cipher.isDeleted) {
return true; return false;
}
return false;
} }
async delete() { if (!(await super.clone())) {
if (await super.delete()) { return false;
this.close();
return true;
}
return false;
} }
close() { this.router.navigate(["/clone-cipher"], {
this.location.back(); queryParams: {
cloneMode: true,
cipherId: this.cipher.id,
},
});
return true;
}
async share() {
if (!(await super.share())) {
return false;
} }
private async loadPageDetails() { if (this.cipher.organizationId == null) {
this.pageDetails = []; this.router.navigate(["/share-cipher"], {
this.tab = await BrowserApi.getTabFromCurrentWindow(); replaceUrl: true,
if (this.tab == null) { queryParams: { cipherId: this.cipher.id },
return; });
}
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, const loginUri = new LoginUriView();
sender: BroadcasterSubscriptionId, 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 (this.pageDetails == null || this.pageDetails.length === 0) {
if (!await this.promptPassword()) { this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
return false; 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;
} }
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;
}
} }

View File

@ -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 { 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 { 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<Account> { export abstract class StateService extends BaseStateServiceAbstraction<Account> {
getBrowserGroupingComponentState: (options?: StorageOptions) => Promise<BrowserGroupingsComponentState>; getBrowserGroupingComponentState: (
setBrowserGroupingComponentState: (value: BrowserGroupingsComponentState, options?: StorageOptions) => Promise<void>; options?: StorageOptions
getBrowserCipherComponentState: (options?: StorageOptions) => Promise<BrowserComponentState>; ) => Promise<BrowserGroupingsComponentState>;
setBrowserCipherComponentState: (value: BrowserComponentState, options?: StorageOptions) => Promise<void>; setBrowserGroupingComponentState: (
getBrowserSendComponentState: (options?: StorageOptions) => Promise<BrowserSendComponentState>; value: BrowserGroupingsComponentState,
setBrowserSendComponentState: (value: BrowserSendComponentState, options?: StorageOptions) => Promise<void>; options?: StorageOptions
getBrowserSendTypeComponentState: (options?: StorageOptions) => Promise<BrowserComponentState>; ) => Promise<void>;
setBrowserSendTypeComponentState: (value: BrowserComponentState, options?: StorageOptions) => Promise<void>; getBrowserCipherComponentState: (options?: StorageOptions) => Promise<BrowserComponentState>;
setBrowserCipherComponentState: (
value: BrowserComponentState,
options?: StorageOptions
) => Promise<void>;
getBrowserSendComponentState: (options?: StorageOptions) => Promise<BrowserSendComponentState>;
setBrowserSendComponentState: (
value: BrowserSendComponentState,
options?: StorageOptions
) => Promise<void>;
getBrowserSendTypeComponentState: (options?: StorageOptions) => Promise<BrowserComponentState>;
setBrowserSendTypeComponentState: (
value: BrowserComponentState,
options?: StorageOptions
) => Promise<void>;
} }

View File

@ -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 { export class BrowserCryptoService extends CryptoService {
protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions) { protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions) {
if (keySuffix === 'biometric') { if (keySuffix === "biometric") {
await this.platformUtilService.authenticateBiometric(); await this.platformUtilService.authenticateBiometric();
return (await this.getKey())?.keyB64; return (await this.getKey())?.keyB64;
}
return await super.retrieveKeyFromStorage(keySuffix);
} }
return await super.retrieveKeyFromStorage(keySuffix);
}
} }

View File

@ -1,340 +1,375 @@
import { BrowserApi } from '../browser/browserApi'; import { BrowserApi } from "../browser/browserApi";
import { SafariApp } from '../browser/safariApp'; import { SafariApp } from "../browser/safariApp";
import { DeviceType } from 'jslib-common/enums/deviceType'; import { DeviceType } from "jslib-common/enums/deviceType";
import { ThemeType } from 'jslib-common/enums/themeType'; import { ThemeType } from "jslib-common/enums/themeType";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from '../services/abstractions/state.service'; import { StateService } from "../services/abstractions/state.service";
const DialogPromiseExpiration = 600000; // 10 minutes const DialogPromiseExpiration = 600000; // 10 minutes
export default class BrowserPlatformUtilsService implements PlatformUtilsService { export default class BrowserPlatformUtilsService implements PlatformUtilsService {
identityClientId: string = 'browser'; identityClientId: string = "browser";
private showDialogResolves = new Map<number, { resolve: (value: boolean) => void, date: Date }>(); private showDialogResolves = new Map<number, { resolve: (value: boolean) => void; date: Date }>();
private passwordDialogResolves = new Map<number, { tryResolve: (canceled: boolean, password: string) => Promise<boolean>, date: Date }>(); private passwordDialogResolves = new Map<
private deviceCache: DeviceType = null; number,
private prefersColorSchemeDark = window.matchMedia('(prefers-color-scheme: dark)'); { tryResolve: (canceled: boolean, password: string) => Promise<boolean>; date: Date }
>();
private deviceCache: DeviceType = null;
private prefersColorSchemeDark = window.matchMedia("(prefers-color-scheme: dark)");
constructor(private messagingService: MessagingService, private stateService: StateService, constructor(
private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, private messagingService: MessagingService,
private biometricCallback: () => Promise<boolean>) { } private stateService: StateService,
private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
private biometricCallback: () => Promise<boolean>
) {}
getDevice(): DeviceType { getDevice(): DeviceType {
if (this.deviceCache) { if (this.deviceCache) {
return 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<boolean> {
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<string> {
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<boolean>((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) { } else if (
this.deviceCache = DeviceType.FirefoxExtension; this.isFirefox() &&
} else if ((!!(window as any).opr && !!opr.addons) || !!(window as any).opera || (win as any).navigator.clipboard &&
navigator.userAgent.indexOf(' OPR/') >= 0) { (win as any).navigator.clipboard.writeText
this.deviceCache = DeviceType.OperaExtension; ) {
} else if (navigator.userAgent.indexOf(' Edg/') !== -1) { (win as any).navigator.clipboard.writeText(text).then(() => {
this.deviceCache = DeviceType.EdgeExtension; if (!clearing && this.clipboardWriteCallback != null) {
} else if (navigator.userAgent.indexOf(' Vivaldi/') !== -1) { this.clipboardWriteCallback(text, clearMs);
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 ((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 { try {
const device = DeviceType[this.getDevice()].toLowerCase(); // Security exception may be thrown by some browsers.
return device.replace('extension', ''); if (doc.execCommand("copy") && !clearing && this.clipboardWriteCallback != null) {
} this.clipboardWriteCallback(text, clearMs);
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<boolean> {
if (await BrowserApi.isPopupOpen()) {
return true;
} }
} catch (e) {
// tslint:disable-next-line
console.warn("Copy to clipboard failed.", e);
} finally {
doc.body.removeChild(textarea);
}
}
}
if (this.isSafari()) { async readFromClipboard(options?: any): Promise<string> {
return false; 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(); resolveDialogPromise(dialogId: number, confirmed: boolean) {
const sidebarOpen = sidebarView != null && chrome.extension.getViews({ type: sidebarView }).length > 0; if (this.showDialogResolves.has(dialogId)) {
if (sidebarOpen) { const resolveObj = this.showDialogResolves.get(dialogId);
return true; resolveObj.resolve(confirmed);
} this.showDialogResolves.delete(dialogId);
const tabOpen = chrome.extension.getViews({ type: 'tab' }).length > 0;
return tabOpen;
} }
lockTimeout(): number { // Clean up old promises
return null; 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<boolean> {
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 { // Clean up old promises
BrowserApi.createNewTab(uri, options && options.extensionPage === true); 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 { if (this.isFirefox()) {
BrowserApi.downloadFile(win, blobData, blobOptions, fileName); return parseInt((await browser.runtime.getBrowserInfo()).version.split(".")[0], 10) >= 87;
} }
getApplicationVersion(): Promise<string> { return true;
return Promise.resolve(BrowserApi.getApplicationVersion()); }
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 null;
return (typeof (PublicKeyCredential) !== 'undefined'); }
}
supportsSecureStorage(): boolean {
supportsDuo(): boolean { return false;
return true; }
}
getDefaultSystemTheme(): Promise<ThemeType.Light | ThemeType.Dark> {
showToast(type: 'error' | 'success' | 'warning' | 'info', title: string, text: string | string[], return Promise.resolve(this.prefersColorSchemeDark.matches ? ThemeType.Dark : ThemeType.Light);
options?: any): void { }
this.messagingService.send('showToast', {
text: text, onDefaultSystemThemeChange(callback: (theme: ThemeType.Light | ThemeType.Dark) => unknown) {
title: title, this.prefersColorSchemeDark.addEventListener("change", ({ matches }) => {
type: type, callback(matches ? ThemeType.Dark : ThemeType.Light);
options: options, });
}); }
}
async getEffectiveTheme() {
showDialog(body: string, title?: string, confirmText?: string, cancelText?: string, type?: string, const theme = (await this.stateService.getTheme()) as ThemeType;
bodyIsHtml: boolean = false) { if (theme == null || theme === ThemeType.System) {
const dialogId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); return this.getDefaultSystemTheme();
this.messagingService.send('showDialog', { } else {
text: bodyIsHtml ? null : body, return theme;
html: bodyIsHtml ? body : null,
title: title,
confirmText: confirmText,
cancelText: cancelText,
type: type,
dialogId: dialogId,
});
return new Promise<boolean>(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<string> {
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<boolean> {
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<ThemeType.Light | ThemeType.Dark> {
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;
}
} }
}
} }

View File

@ -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 { export default class BrowserStorageService implements StorageService {
private chromeStorageApi: any; private chromeStorageApi: any;
constructor() { constructor() {
this.chromeStorageApi = chrome.storage.local; this.chromeStorageApi = chrome.storage.local;
} }
async get<T>(key: string): Promise<T> { async get<T>(key: string): Promise<T> {
return new Promise(resolve => { return new Promise((resolve) => {
this.chromeStorageApi.get(key, (obj: any) => { this.chromeStorageApi.get(key, (obj: any) => {
if (obj != null && obj[key] != null) { if (obj != null && obj[key] != null) {
resolve(obj[key] as T); resolve(obj[key] as T);
return; return;
}
resolve(null);
});
});
}
async has(key: string): Promise<boolean> {
return await this.get(key) != null;
}
async save(key: string, obj: any): Promise<any> {
if (obj == null) {
// Fix safari not liking null in set
return this.remove(key);
} }
resolve(null);
});
});
}
if (obj instanceof Set) { async has(key: string): Promise<boolean> {
obj = Array.from(obj); return (await this.get(key)) != null;
} }
const keyedObj = { [key]: obj }; async save(key: string, obj: any): Promise<any> {
return new Promise<void>(resolve => { if (obj == null) {
this.chromeStorageApi.set(keyedObj, () => { // Fix safari not liking null in set
resolve(); return this.remove(key);
});
});
} }
async remove(key: string): Promise<any> { if (obj instanceof Set) {
return new Promise<void>(resolve => { obj = Array.from(obj);
this.chromeStorageApi.remove(key, () => {
resolve();
});
});
} }
const keyedObj = { [key]: obj };
return new Promise<void>((resolve) => {
this.chromeStorageApi.set(keyedObj, () => {
resolve();
});
});
}
async remove(key: string): Promise<any> {
return new Promise<void>((resolve) => {
this.chromeStorageApi.remove(key, () => {
resolve();
});
});
}
} }

View File

@ -1,52 +1,75 @@
import { StorageOptions } from 'jslib-common/models/domain/storageOptions'; import { StorageOptions } from "jslib-common/models/domain/storageOptions";
import { StateService as BaseStateService } from 'jslib-common/services/state.service'; 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 { 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<Account> implements StateServiceAbstraction { export class StateService extends BaseStateService<Account> implements StateServiceAbstraction {
async getBrowserGroupingComponentState(
options?: StorageOptions
): Promise<BrowserGroupingsComponentState> {
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
?.groupings;
}
async getBrowserGroupingComponentState(options?: StorageOptions): Promise<BrowserGroupingsComponentState> { async setBrowserGroupingComponentState(
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.groupings; value: BrowserGroupingsComponentState,
} options?: StorageOptions
): Promise<void> {
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<void> { async getBrowserCipherComponentState(options?: StorageOptions): Promise<BrowserComponentState> {
const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
account.groupings = value; ?.ciphers;
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); }
}
async getBrowserCipherComponentState(options?: StorageOptions): Promise<BrowserComponentState> { async setBrowserCipherComponentState(
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.ciphers; value: BrowserComponentState,
} options?: StorageOptions
): Promise<void> {
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<void> { async getBrowserSendComponentState(options?: StorageOptions): Promise<BrowserSendComponentState> {
const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
account.ciphers = value; ?.send;
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); }
}
async getBrowserSendComponentState(options?: StorageOptions): Promise<BrowserSendComponentState> { async setBrowserSendComponentState(
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.send; value: BrowserSendComponentState,
} options?: StorageOptions
): Promise<void> {
async setBrowserSendComponentState(value: BrowserSendComponentState, options?: StorageOptions): Promise<void> { const account = await this.getAccount(
const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)); this.reconcileOptions(options, this.defaultInMemoryOptions)
account.send = value; );
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); account.send = value;
} await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
async getBrowserSendTypeComponentState(options?: StorageOptions): Promise<BrowserComponentState> { }
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.sendType; async getBrowserSendTypeComponentState(options?: StorageOptions): Promise<BrowserComponentState> {
} return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
?.sendType;
async setBrowserSendTypeComponentState(value: BrowserComponentState, options?: StorageOptions): Promise<void> { }
const account = await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions));
account.sendType = value;
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
}
async setBrowserSendTypeComponentState(
value: BrowserComponentState,
options?: StorageOptions
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, this.defaultInMemoryOptions)
);
account.sendType = value;
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
}
} }