bitwarden-estensione-browser/apps/browser/src/platform/services/browser-state.service.ts

216 lines
7.5 KiB
TypeScript

import { BehaviorSubject } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import {
AbstractStorageService,
AbstractMemoryStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
import { Account } from "../../models/account";
import { BrowserComponentState } from "../../models/browserComponentState";
import { BrowserGroupingsComponentState } from "../../models/browserGroupingsComponentState";
import { BrowserSendComponentState } from "../../models/browserSendComponentState";
import { BrowserApi } from "../browser/browser-api";
import { browserSession, sessionSync } from "../decorators/session-sync-observable";
import { BrowserStateService as StateServiceAbstraction } from "./abstractions/browser-state.service";
@browserSession
export class BrowserStateService
extends BaseStateService<GlobalState, Account>
implements StateServiceAbstraction
{
@sessionSync({
initializer: Account.fromJSON as any, // TODO: Remove this any when all any types are removed from Account
initializeAs: "record",
})
protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>;
@sessionSync({ initializer: (s: string) => s })
protected activeAccountSubject: BehaviorSubject<string>;
protected accountDeserializer = Account.fromJSON;
constructor(
storageService: AbstractStorageService,
secureStorageService: AbstractStorageService,
memoryStorageService: AbstractMemoryStorageService,
logService: LogService,
stateFactory: StateFactory<GlobalState, Account>,
accountService: AccountService,
environmentService: EnvironmentService,
tokenService: TokenService,
migrationRunner: MigrationRunner,
useAccountCache = true,
) {
super(
storageService,
secureStorageService,
memoryStorageService,
logService,
stateFactory,
accountService,
environmentService,
tokenService,
migrationRunner,
useAccountCache,
);
// TODO: This is a hack to fix having a disk cache on both the popup and
// the background page that can get out of sync. We need to work out the
// best way to handle caching with multiple instances of the state service.
if (useAccountCache) {
BrowserApi.storageChangeListener((changes, namespace) => {
if (namespace === "local") {
for (const key of Object.keys(changes)) {
if (key !== "accountActivity" && this.accountDiskCache.value[key]) {
this.deleteDiskCache(key);
}
}
}
});
BrowserApi.addListener(
chrome.runtime.onMessage,
(message: { command: string }, _, respond) => {
if (message.command === "initializeDiskCache") {
respond(JSON.stringify(this.accountDiskCache.value));
}
},
);
}
}
override async initAccountState(): Promise<void> {
if (this.isRecoveredSession && this.useAccountCache) {
// request cache initialization
const response = await BrowserApi.sendMessageWithResponse<string>("initializeDiskCache");
this.accountDiskCache.next(JSON.parse(response));
return;
}
await super.initAccountState();
}
async addAccount(account: Account) {
// Apply browser overrides to default account values
account = new Account(account);
await super.addAccount(account);
}
async getIsAuthenticated(options?: StorageOptions): Promise<boolean> {
// Firefox Private Mode can clash with non-Private Mode because they both read from the same onDiskOptions
// Check that there is an account in memory before considering the user authenticated
return (
(await super.getIsAuthenticated(options)) &&
(await this.getAccount(await this.defaultInMemoryOptions())) != null
);
}
async getBrowserGroupingComponentState(
options?: StorageOptions,
): Promise<BrowserGroupingsComponentState> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.groupings;
}
async setBrowserGroupingComponentState(
value: BrowserGroupingsComponentState,
options?: StorageOptions,
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
account.groupings = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
}
async getBrowserVaultItemsComponentState(
options?: StorageOptions,
): Promise<BrowserComponentState> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.ciphers;
}
async setBrowserVaultItemsComponentState(
value: BrowserComponentState,
options?: StorageOptions,
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
account.ciphers = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
}
async getBrowserSendComponentState(options?: StorageOptions): Promise<BrowserSendComponentState> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.send;
}
async setBrowserSendComponentState(
value: BrowserSendComponentState,
options?: StorageOptions,
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
account.send = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
}
async getBrowserSendTypeComponentState(options?: StorageOptions): Promise<BrowserComponentState> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.sendType;
}
async setBrowserSendTypeComponentState(
value: BrowserComponentState,
options?: StorageOptions,
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
account.sendType = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
}
// Overriding the base class to prevent deleting the cache on save. We register a storage listener
// to delete the cache in the constructor above.
protected override async saveAccountToDisk(
account: Account,
options: StorageOptions,
): Promise<void> {
const storageLocation = options.useSecureStorage
? this.secureStorageService
: this.storageService;
await storageLocation.save(`${options.userId}`, account, options);
}
}