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:
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user