From 1c4d851046eca62a39e4c82c58f898eebd0b0946 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 21 May 2025 08:00:49 -0500 Subject: [PATCH] [PM-21005] Clear Add/Edit form cache when browser loses focus (#14634) --- .../view-cache/popup-view-cache.service.ts | 2 + .../popup-view-cache-background.service.ts | 40 ++++++++++++++++++- .../add-edit/add-edit-v2.component.ts | 22 ++++++++-- .../new-item-dropdown-v2.component.spec.ts | 3 +- .../new-item-dropdown-v2.component.ts | 14 +++---- .../platform/view-cache/view-cache.service.ts | 6 +++ .../default-cipher-form-cache.service.ts | 1 + 7 files changed, 73 insertions(+), 15 deletions(-) diff --git a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts index 2a94698299..83d6edbc14 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts @@ -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( diff --git a/apps/browser/src/platform/services/popup-view-cache-background.service.ts b/apps/browser/src/platform/services/popup-view-cache-background.service.ts index 79c04e90aa..49eae15fbb 100644 --- a/apps/browser/src/platform/services/popup-view-cache-background.service.ts +++ b/apps/browser/src/platform/services/popup-view-cache-background.service.ts @@ -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() { diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index 30fd57a6bc..e47f463719 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -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>; @@ -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; } diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts index a9b92274c9..54c6ba2f78 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts @@ -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", }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts index bb452b89c7..1bcc4297b7 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts @@ -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 { diff --git a/libs/angular/src/platform/view-cache/view-cache.service.ts b/libs/angular/src/platform/view-cache/view-cache.service.ts index 386ddc7bdd..ed74ac0ba5 100644 --- a/libs/angular/src/platform/view-cache/view-cache.service.ts +++ b/libs/angular/src/platform/view-cache/view-cache.service.ts @@ -23,6 +23,12 @@ type BaseCacheOptions = { * 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 : Required>); export type SignalCacheOptions = BaseCacheOptions & { diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts index b4a8138e02..521cc09f14 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts @@ -27,6 +27,7 @@ export class CipherFormCacheService { key: CIPHER_FORM_CACHE_KEY, initialValue: null, deserializer: CipherView.fromJSON, + clearOnTabChange: true, }); constructor() {