1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 00:03:56 +00:00

[PM-24929] Clear route history cache for add/edit cipher views on tab change (#16755)

* clear route history cache for add/edit cipher views on tab change

* remove duplicate words
This commit is contained in:
Nick Krantz
2025-10-22 08:36:31 -05:00
committed by GitHub
parent 453feb362f
commit 7418f67874
6 changed files with 147 additions and 23 deletions

View File

@@ -319,6 +319,7 @@ import I18nService from "../platform/services/i18n.service";
import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service";
import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service"; import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service";
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
import { PopupRouterCacheBackgroundService } from "../platform/services/popup-router-cache-background.service";
import { PopupViewCacheBackgroundService } from "../platform/services/popup-view-cache-background.service"; import { PopupViewCacheBackgroundService } from "../platform/services/popup-view-cache-background.service";
import { BrowserSdkLoadService } from "../platform/services/sdk/browser-sdk-load.service"; import { BrowserSdkLoadService } from "../platform/services/sdk/browser-sdk-load.service";
import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service"; import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service";
@@ -488,6 +489,7 @@ export default class MainBackground {
private nativeMessagingBackground: NativeMessagingBackground; private nativeMessagingBackground: NativeMessagingBackground;
private popupViewCacheBackgroundService: PopupViewCacheBackgroundService; private popupViewCacheBackgroundService: PopupViewCacheBackgroundService;
private popupRouterCacheBackgroundService: PopupRouterCacheBackgroundService;
constructor() { constructor() {
// Services // Services
@@ -684,6 +686,9 @@ export default class MainBackground {
this.globalStateProvider, this.globalStateProvider,
this.taskSchedulerService, this.taskSchedulerService,
); );
this.popupRouterCacheBackgroundService = new PopupRouterCacheBackgroundService(
this.globalStateProvider,
);
this.migrationRunner = new MigrationRunner( this.migrationRunner = new MigrationRunner(
this.storageService, this.storageService,
@@ -1514,6 +1519,7 @@ export default class MainBackground {
(this.eventUploadService as EventUploadService).init(true); (this.eventUploadService as EventUploadService).init(true);
this.popupViewCacheBackgroundService.startObservingMessages(); this.popupViewCacheBackgroundService.startObservingMessages();
this.popupRouterCacheBackgroundService.init();
await this.vaultTimeoutService.init(true); await this.vaultTimeoutService.init(true);
this.fido2Background.init(); this.fido2Background.init();

View File

@@ -5,6 +5,7 @@ import { Injectable, inject } from "@angular/core";
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivateFn, CanActivateFn,
Data,
NavigationEnd, NavigationEnd,
Router, Router,
UrlSerializer, UrlSerializer,
@@ -14,7 +15,10 @@ import { filter, first, firstValueFrom, map, Observable, of, switchMap, tap } fr
import { GlobalStateProvider } from "@bitwarden/common/platform/state"; import { GlobalStateProvider } from "@bitwarden/common/platform/state";
import { POPUP_ROUTE_HISTORY_KEY } from "../../../platform/services/popup-view-cache-background.service"; import {
POPUP_ROUTE_HISTORY_KEY,
RouteHistoryCacheState,
} from "../../../platform/services/popup-view-cache-background.service";
import BrowserPopupUtils from "../../browser/browser-popup-utils"; import BrowserPopupUtils from "../../browser/browser-popup-utils";
/** /**
@@ -42,8 +46,7 @@ export class PopupRouterCacheService {
this.history$() this.history$()
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(history) => (history) => Array.isArray(history) && history.forEach(({ url }) => this.location.go(url)),
Array.isArray(history) && history.forEach((location) => this.location.go(location)),
); );
// update state when route change occurs // update state when route change occurs
@@ -54,31 +57,33 @@ export class PopupRouterCacheService {
// `Location.back()` can now be called successfully // `Location.back()` can now be called successfully
this.hasNavigated = true; this.hasNavigated = true;
}), }),
filter((_event: NavigationEnd) => { map((event) => {
const state: ActivatedRouteSnapshot = this.router.routerState.snapshot.root; const state: ActivatedRouteSnapshot = this.router.routerState.snapshot.root;
let child = state.firstChild; let child = state.firstChild;
while (child.firstChild) { while (child.firstChild) {
child = child.firstChild; child = child.firstChild;
} }
return { event, data: child.data };
return !child?.data?.doNotSaveUrl;
}), }),
switchMap((event) => this.push(event.url)), filter(({ data }) => {
return !data?.doNotSaveUrl;
}),
switchMap(({ event, data }) => this.push(event.url, data)),
) )
.subscribe(); .subscribe();
} }
history$(): Observable<string[]> { history$(): Observable<RouteHistoryCacheState[]> {
return this.state.state$; return this.state.state$;
} }
async setHistory(state: string[]): Promise<string[]> { async setHistory(state: RouteHistoryCacheState[]): Promise<RouteHistoryCacheState[]> {
return this.state.update(() => state); return this.state.update(() => state);
} }
/** Get the last item from the history stack, or `null` if empty */ /** Get the last item from the history stack, or `null` if empty */
last$(): Observable<string | null> { last$(): Observable<RouteHistoryCacheState | null> {
return this.history$().pipe( return this.history$().pipe(
map((history) => { map((history) => {
if (!history || history.length === 0) { if (!history || history.length === 0) {
@@ -92,11 +97,24 @@ export class PopupRouterCacheService {
/** /**
* If in browser popup, push new route onto history stack * If in browser popup, push new route onto history stack
*/ */
private async push(url: string) { private async push(url: string, data: Data) {
if (!BrowserPopupUtils.inPopup(window) || url === (await firstValueFrom(this.last$()))) { if (
!BrowserPopupUtils.inPopup(window) ||
url === (await firstValueFrom(this.last$().pipe(map((h) => h?.url))))
) {
return; return;
} }
await this.state.update((prevState) => (prevState == null ? [url] : prevState.concat(url)));
const routeEntry: RouteHistoryCacheState = {
url,
options: {
resetRouterCacheOnTabChange: data?.resetRouterCacheOnTabChange ?? false,
},
};
await this.state.update((prevState) =>
prevState == null ? [routeEntry] : prevState.concat(routeEntry),
);
} }
/** /**
@@ -142,13 +160,13 @@ export const popupRouterCacheGuard = ((): Observable<true | UrlTree> => {
} }
return popupHistoryService.last$().pipe( return popupHistoryService.last$().pipe(
map((url: string) => { map((entry) => {
if (!url) { if (!entry) {
return true; return true;
} }
popupHistoryService.markCacheRestored(); popupHistoryService.markCacheRestored();
return urlSerializer.parse(url); return urlSerializer.parse(entry.url);
}), }),
); );
}) satisfies CanActivateFn; }) satisfies CanActivateFn;

View File

@@ -96,11 +96,31 @@ describe("Popup router cache guard", () => {
// wait for router events subscription // wait for router events subscription
await flushPromises(); await flushPromises();
expect(await firstValueFrom(service.history$())).toEqual(["/a", "/b"]); expect(await firstValueFrom(service.history$())).toEqual([
{
options: {
resetRouterCacheOnTabChange: false,
},
url: "/a",
},
{
options: {
resetRouterCacheOnTabChange: false,
},
url: "/b",
},
]);
await service.back(); await service.back();
expect(await firstValueFrom(service.history$())).toEqual(["/a"]); expect(await firstValueFrom(service.history$())).toEqual([
{
options: {
resetRouterCacheOnTabChange: false,
},
url: "/a",
},
]);
}); });
it("does not save ignored routes", async () => { it("does not save ignored routes", async () => {
@@ -121,6 +141,13 @@ describe("Popup router cache guard", () => {
await flushPromises(); await flushPromises();
expect(await firstValueFrom(service.history$())).toEqual(["/a"]); expect(await firstValueFrom(service.history$())).toEqual([
{
options: {
resetRouterCacheOnTabChange: false,
},
url: "/a",
},
]);
}); });
}); });

View File

@@ -0,0 +1,55 @@
import { switchMap, filter, map, first, of } from "rxjs";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
import { BrowserApi } from "../browser/browser-api";
import { fromChromeEvent } from "../browser/from-chrome-event";
import { POPUP_ROUTE_HISTORY_KEY } from "./popup-view-cache-background.service";
export class PopupRouterCacheBackgroundService {
private popupRouteHistoryState = this.globalStateProvider.get(POPUP_ROUTE_HISTORY_KEY);
constructor(private globalStateProvider: GlobalStateProvider) {}
init() {
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.popupRouteHistoryState.update((state) => {
if (!state || state.length === 0) {
return state;
}
const lastRoute = state.at(-1);
if (!lastRoute) {
return state;
}
// When the last route has resetRouterCacheOnTabChange set
// Reset the route history to empty to force the user to the default route
if (lastRoute.options?.resetRouterCacheOnTabChange) {
return [];
}
return state;
}),
),
)
.subscribe();
}
}

View File

@@ -42,6 +42,22 @@ export type ViewCacheState = {
options?: ViewCacheOptions; options?: ViewCacheOptions;
}; };
export type RouteCacheOptions = {
/**
* When true, the route history will be reset on tab change and respective route was the last visited route.
* i.e. Upon the user re-opening the extension the route history will be empty and the user will be taken to the default route.
*/
resetRouterCacheOnTabChange?: boolean;
};
export type RouteHistoryCacheState = {
/** Route URL */
url: string;
/** Options for managing the route history cache */
options?: RouteCacheOptions;
};
/** We cannot use `UserKeyDefinition` because we must be able to store state when there is no active user. */ /** We cannot use `UserKeyDefinition` because we must be able to store state when there is no active user. */
export const POPUP_VIEW_CACHE_KEY = KeyDefinition.record<ViewCacheState>( export const POPUP_VIEW_CACHE_KEY = KeyDefinition.record<ViewCacheState>(
POPUP_VIEW_MEMORY, POPUP_VIEW_MEMORY,
@@ -51,9 +67,9 @@ export const POPUP_VIEW_CACHE_KEY = KeyDefinition.record<ViewCacheState>(
}, },
); );
export const POPUP_ROUTE_HISTORY_KEY = new KeyDefinition<string[]>( export const POPUP_ROUTE_HISTORY_KEY = new KeyDefinition<RouteHistoryCacheState[]>(
POPUP_VIEW_MEMORY, POPUP_VIEW_MEMORY,
"popup-route-history", "popup-route-history-details",
{ {
deserializer: (jsonValue) => jsonValue, deserializer: (jsonValue) => jsonValue,
}, },

View File

@@ -59,6 +59,7 @@ import { ProtectedByComponent } from "../dirt/phishing-detection/pages/protected
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import BrowserPopupUtils from "../platform/browser/browser-popup-utils"; import BrowserPopupUtils from "../platform/browser/browser-popup-utils";
import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service"; import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service";
import { RouteCacheOptions } from "../platform/services/popup-view-cache-background.service";
import { CredentialGeneratorHistoryComponent } from "../tools/popup/generator/credential-generator-history.component"; import { CredentialGeneratorHistoryComponent } from "../tools/popup/generator/credential-generator-history.component";
import { CredentialGeneratorComponent } from "../tools/popup/generator/credential-generator.component"; import { CredentialGeneratorComponent } from "../tools/popup/generator/credential-generator.component";
import { SendAddEditComponent as SendAddEditV2Component } from "../tools/popup/send-v2/add-edit/send-add-edit.component"; import { SendAddEditComponent as SendAddEditV2Component } from "../tools/popup/send-v2/add-edit/send-add-edit.component";
@@ -98,7 +99,7 @@ import { TabsV2Component } from "./tabs-v2.component";
/** /**
* Data properties acceptable for use in extension route objects * Data properties acceptable for use in extension route objects
*/ */
export interface RouteDataProperties { export interface RouteDataProperties extends RouteCacheOptions {
elevation: RouteElevation; elevation: RouteElevation;
/** /**
@@ -204,7 +205,7 @@ const routes: Routes = [
path: "add-cipher", path: "add-cipher",
component: AddEditV2Component, component: AddEditV2Component,
canActivate: [authGuard, debounceNavigationGuard()], canActivate: [authGuard, debounceNavigationGuard()],
data: { elevation: 1 } satisfies RouteDataProperties, data: { elevation: 1, resetRouterCacheOnTabChange: true } satisfies RouteDataProperties,
runGuardsAndResolvers: "always", runGuardsAndResolvers: "always",
}, },
{ {
@@ -214,6 +215,7 @@ const routes: Routes = [
data: { data: {
// Above "trash" // Above "trash"
elevation: 3, elevation: 3,
resetRouterCacheOnTabChange: true,
} satisfies RouteDataProperties, } satisfies RouteDataProperties,
runGuardsAndResolvers: "always", runGuardsAndResolvers: "always",
}, },