1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-21005] Clear Add/Edit form cache when browser loses focus (#14634)

This commit is contained in:
Nick Krantz
2025-05-21 08:00:49 -05:00
committed by GitHub
parent ae35cb4e65
commit 1c4d851046
7 changed files with 73 additions and 15 deletions

View File

@@ -81,6 +81,7 @@ export class PopupViewCacheService implements ViewCacheService {
injector = inject(Injector),
initialValue,
persistNavigation,
clearOnTabChange,
} = options;
const cachedValue = this.cache[key]?.value
? deserializer(JSON.parse(this.cache[key].value))
@@ -89,6 +90,7 @@ export class PopupViewCacheService implements ViewCacheService {
const viewCacheOptions = {
...(persistNavigation && { persistNavigation }),
...(clearOnTabChange && { clearOnTabChange }),
};
effect(

View File

@@ -1,4 +1,4 @@
import { switchMap, delay, filter, concatMap } from "rxjs";
import { switchMap, delay, filter, concatMap, map, first, of } from "rxjs";
import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging";
import {
@@ -12,6 +12,7 @@ import {
GlobalStateProvider,
} from "@bitwarden/common/platform/state";
import { BrowserApi } from "../browser/browser-api";
import { fromChromeEvent } from "../browser/from-chrome-event";
const popupClosedPortName = "new_popup";
@@ -21,6 +22,12 @@ export type ViewCacheOptions = {
* Optional flag to persist the cached value between navigation events.
*/
persistNavigation?: boolean;
/**
* When set, the cached value will be cleared when the user changes tabs.
* @optional
*/
clearOnTabChange?: true;
};
export type ViewCacheState = {
@@ -129,6 +136,37 @@ export class PopupViewCacheBackgroundService {
),
)
.subscribe();
// On tab changed, excluding extension tabs
fromChromeEvent(chrome.tabs.onActivated)
.pipe(
switchMap((tabs) => BrowserApi.getTab(tabs[0].tabId)!),
switchMap((tab) => {
// FireFox sets the `url` to "about:blank" and won't populate the `url` until the `onUpdated` event
if (tab.url !== "about:blank") {
return of(tab);
}
return fromChromeEvent(chrome.tabs.onUpdated).pipe(
first(),
switchMap(([tabId]) => BrowserApi.getTab(tabId)!),
);
}),
map((tab) => tab.url || tab.pendingUrl),
filter((url) => !url?.startsWith(chrome.runtime.getURL(""))),
switchMap(() =>
this.popupViewCacheState.update((state) => {
if (!state) {
return null;
}
// Only remove keys that are marked with `clearOnTabChange`
return Object.fromEntries(
Object.entries(state).filter(([, { options }]) => !options?.clearOnTabChange),
);
}),
),
)
.subscribe();
}
async clearState() {

View File

@@ -14,6 +14,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
@@ -40,6 +41,7 @@ import {
} from "@bitwarden/vault";
import { BrowserFido2UserInterfaceSession } from "../../../../../autofill/fido2/services/browser-fido2-user-interface.service";
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component";
@@ -70,6 +72,7 @@ class QueryParams {
this.uri = params.uri;
this.username = params.username;
this.name = params.name;
this.prefillNameAndURIFromTab = params.prefillNameAndURIFromTab;
}
/**
@@ -116,6 +119,12 @@ class QueryParams {
* Optional name to pre-fill for the cipher.
*/
name?: string;
/**
* Optional flag to pre-fill the name and URI from the current tab.
* NOTE: This will override the `uri` and `name` query parameters if set to true.
*/
prefillNameAndURIFromTab?: true;
}
export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
@@ -281,8 +290,7 @@ export class AddEditV2Component implements OnInit {
if (config.mode === "edit" && !config.originalCipher.edit) {
config.mode = "partial-edit";
}
config.initialValues = this.setInitialValuesFromParams(params);
config.initialValues = await this.setInitialValuesFromParams(params);
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(getUserId),
@@ -326,7 +334,7 @@ export class AddEditV2Component implements OnInit {
});
}
setInitialValuesFromParams(params: QueryParams) {
async setInitialValuesFromParams(params: QueryParams) {
const initialValues = {} as OptionalInitialValues;
if (params.folderId) {
initialValues.folderId = params.folderId;
@@ -346,6 +354,14 @@ export class AddEditV2Component implements OnInit {
if (params.name) {
initialValues.name = params.name;
}
if (params.prefillNameAndURIFromTab) {
const tab = await BrowserApi.getTabFromCurrentWindow();
initialValues.loginUri = tab.url;
initialValues.name = Utils.getHostname(tab.url);
}
return initialValues;
}

View File

@@ -94,8 +94,7 @@ describe("NewItemDropdownV2Component", () => {
collectionId: "777-888-999",
organizationId: "444-555-666",
folderId: "222-333-444",
uri: "https://example.com",
name: "example.com",
prefillNameAndURIFromTab: "true",
});
});

View File

@@ -2,10 +2,9 @@
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core";
import { Router, RouterLink } from "@angular/router";
import { RouterLink } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
@@ -35,10 +34,8 @@ export class NewItemDropdownV2Component implements OnInit {
*/
@Input()
initialValues: NewItemInitialValues;
constructor(
private router: Router,
private dialogService: DialogService,
) {}
constructor(private dialogService: DialogService) {}
async ngOnInit() {
this.tab = await BrowserApi.getTabFromCurrentWindow();
@@ -47,13 +44,12 @@ export class NewItemDropdownV2Component implements OnInit {
buildQueryParams(type: CipherType): AddEditQueryParams {
const poppedOut = BrowserPopupUtils.inPopout(window);
const loginDetails: { uri?: string; name?: string } = {};
const loginDetails: { prefillNameAndURIFromTab?: string } = {};
// When a Login Cipher is created and the extension is not popped out,
// pass along the uri and name
if (!poppedOut && type === CipherType.Login && this.tab) {
loginDetails.uri = this.tab.url;
loginDetails.name = Utils.getHostname(this.tab.url);
loginDetails.prefillNameAndURIFromTab = "true";
}
return {

View File

@@ -23,6 +23,12 @@ type BaseCacheOptions<T> = {
* Optional flag to persist the cached value between navigation events.
*/
persistNavigation?: boolean;
/**
* When set, the cached value will be cleared when the user changes tabs.
* @optional
*/
clearOnTabChange?: true;
} & (T extends JsonValue ? Deserializer<T> : Required<Deserializer<T>>);
export type SignalCacheOptions<T> = BaseCacheOptions<T> & {

View File

@@ -27,6 +27,7 @@ export class CipherFormCacheService {
key: CIPHER_FORM_CACHE_KEY,
initialValue: null,
deserializer: CipherView.fromJSON,
clearOnTabChange: true,
});
constructor() {