1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 22:03:36 +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), injector = inject(Injector),
initialValue, initialValue,
persistNavigation, persistNavigation,
clearOnTabChange,
} = options; } = options;
const cachedValue = this.cache[key]?.value const cachedValue = this.cache[key]?.value
? deserializer(JSON.parse(this.cache[key].value)) ? deserializer(JSON.parse(this.cache[key].value))
@@ -89,6 +90,7 @@ export class PopupViewCacheService implements ViewCacheService {
const viewCacheOptions = { const viewCacheOptions = {
...(persistNavigation && { persistNavigation }), ...(persistNavigation && { persistNavigation }),
...(clearOnTabChange && { clearOnTabChange }),
}; };
effect( 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 { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging";
import { import {
@@ -12,6 +12,7 @@ import {
GlobalStateProvider, GlobalStateProvider,
} from "@bitwarden/common/platform/state"; } from "@bitwarden/common/platform/state";
import { BrowserApi } from "../browser/browser-api";
import { fromChromeEvent } from "../browser/from-chrome-event"; import { fromChromeEvent } from "../browser/from-chrome-event";
const popupClosedPortName = "new_popup"; const popupClosedPortName = "new_popup";
@@ -21,6 +22,12 @@ export type ViewCacheOptions = {
* Optional flag to persist the cached value between navigation events. * Optional flag to persist the cached value between navigation events.
*/ */
persistNavigation?: boolean; persistNavigation?: boolean;
/**
* When set, the cached value will be cleared when the user changes tabs.
* @optional
*/
clearOnTabChange?: true;
}; };
export type ViewCacheState = { export type ViewCacheState = {
@@ -129,6 +136,37 @@ export class PopupViewCacheBackgroundService {
), ),
) )
.subscribe(); .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() { async clearState() {

View File

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

View File

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

View File

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

View File

@@ -23,6 +23,12 @@ type BaseCacheOptions<T> = {
* Optional flag to persist the cached value between navigation events. * Optional flag to persist the cached value between navigation events.
*/ */
persistNavigation?: boolean; 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>>); } & (T extends JsonValue ? Deserializer<T> : Required<Deserializer<T>>);
export type SignalCacheOptions<T> = BaseCacheOptions<T> & { export type SignalCacheOptions<T> = BaseCacheOptions<T> & {

View File

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