diff --git a/apps/browser/src/background.ts b/apps/browser/src/background.ts index f55cc4b1962..504b5afb5c0 100644 --- a/apps/browser/src/background.ts +++ b/apps/browser/src/background.ts @@ -1,17 +1,31 @@ import MainBackground from "./background/main.background"; +import { BrowserApi } from "./browser/browserApi"; import { ClearClipboard } from "./clipboard"; import { onCommandListener } from "./listeners/onCommandListener"; import { onInstallListener } from "./listeners/onInstallListener"; +import { UpdateBadge } from "./listeners/update-badge"; +const manifestV3MessageListeners: (( + serviceCache: Record, + message: { command: string } +) => void | Promise)[] = [UpdateBadge.messageListener]; type AlarmAction = (executionTime: Date, serviceCache: Record) => void; const AlarmActions: AlarmAction[] = [ClearClipboard.run]; -const manifest = chrome.runtime.getManifest(); - -if (manifest.manifest_version === 3) { +if (BrowserApi.manifestVersion === 3) { chrome.commands.onCommand.addListener(onCommandListener); chrome.runtime.onInstalled.addListener(onInstallListener); + chrome.tabs.onActivated.addListener(UpdateBadge.tabsOnActivatedListener); + chrome.tabs.onReplaced.addListener(UpdateBadge.tabsOnReplacedListener); + chrome.tabs.onUpdated.addListener(UpdateBadge.tabsOnUpdatedListener); + BrowserApi.messageListener("runtime.background", (message) => { + const serviceCache = {}; + + manifestV3MessageListeners.forEach((listener) => { + listener(serviceCache, message); + }); + }); chrome.alarms.onAlarm.addListener((_alarm) => { const executionTime = new Date(); const serviceCache = {}; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index a580b474bef..c90d58ec138 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -82,6 +82,7 @@ import { WebCryptoFunctionService } from "@bitwarden/common/services/webCryptoFu import { BrowserApi } from "../browser/browserApi"; import { SafariApp } from "../browser/safariApp"; +import { UpdateBadge } from "../listeners/update-badge"; import { Account } from "../models/account"; import { PopupUtilsService } from "../popup/services/popup-utils.service"; import { AutofillService as AutofillServiceAbstraction } from "../services/abstractions/autofill.service"; @@ -183,15 +184,18 @@ export default class MainBackground { private syncTimeout: any; private isSafari: boolean; private nativeMessagingBackground: NativeMessagingBackground; + popupOnlyContext: boolean; constructor(public isPrivateMode: boolean = false) { + this.popupOnlyContext = isPrivateMode || BrowserApi.manifestVersion === 3; + // Services const lockedCallback = async (userId?: string) => { if (this.notificationsService != null) { this.notificationsService.updateConnection(false); } - await this.setIcon(); - await this.refreshBadgeAndMenu(true); + await this.refreshBadge(); + await this.refreshMenu(true); if (this.systemService != null) { await this.systemService.clearPendingClipboard(); await this.systemService.startProcessReload(this.authService); @@ -201,7 +205,7 @@ export default class MainBackground { const logoutCallback = async (expired: boolean, userId?: string) => await this.logout(expired, userId); - this.messagingService = isPrivateMode + this.messagingService = this.popupOnlyContext ? new BrowserMessagingPrivateModeBackgroundService() : new BrowserMessagingService(); this.logService = new ConsoleLogService(false); @@ -209,7 +213,7 @@ export default class MainBackground { this.storageService = new BrowserLocalStorageService(); this.secureStorageService = new BrowserLocalStorageService(); this.memoryStorageService = - chrome.runtime.getManifest().manifest_version == 3 + BrowserApi.manifestVersion === 3 ? new LocalBackedSessionStorageService( new EncryptService(this.cryptoFunctionService, this.logService, false), new KeyGenerationService(this.cryptoFunctionService) @@ -562,14 +566,12 @@ export default class MainBackground { // Set Private Mode windows to the default icon - they do not share state with the background page const privateWindows = await BrowserApi.getPrivateModeWindows(); privateWindows.forEach(async (win) => { - await this.actionSetIcon(chrome.browserAction, "", win.id); - await this.actionSetIcon(this.sidebarAction, "", win.id); + await new UpdateBadge(self).setBadgeIcon("", win.id); }); BrowserApi.onWindowCreated(async (win) => { if (win.incognito) { - await this.actionSetIcon(chrome.browserAction, "", win.id); - await this.actionSetIcon(this.sidebarAction, "", win.id); + await new UpdateBadge(self).setBadgeIcon("", win.id); } }); } @@ -577,7 +579,7 @@ export default class MainBackground { return new Promise((resolve) => { setTimeout(async () => { await this.environmentService.setUrlsFromStorage(); - await this.setIcon(); + await this.refreshBadge(); this.fullSync(true); setTimeout(() => this.notificationsService.init(), 2500); resolve(); @@ -585,25 +587,11 @@ export default class MainBackground { }); } - async setIcon() { - if ((!chrome.browserAction && !this.sidebarAction) || this.isPrivateMode) { - return; - } - - const authStatus = await this.authService.getAuthStatus(); - - let suffix = ""; - if (authStatus === AuthenticationStatus.LoggedOut) { - suffix = "_gray"; - } else if (authStatus === AuthenticationStatus.Locked) { - suffix = "_locked"; - } - - await this.actionSetIcon(chrome.browserAction, suffix); - await this.actionSetIcon(this.sidebarAction, suffix); + async refreshBadge() { + await new UpdateBadge(self).run({ existingServices: this as any }); } - async refreshBadgeAndMenu(forLocked = false) { + async refreshMenu(forLocked = false) { if (!chrome.windows || !chrome.contextMenus) { return; } @@ -616,7 +604,7 @@ export default class MainBackground { } if (forLocked) { - await this.loadMenuAndUpdateBadgeForNoAccessState(!menuDisabled); + await this.loadMenuForNoAccessState(!menuDisabled); this.onUpdatedRan = this.onReplacedRan = false; return; } @@ -652,8 +640,11 @@ export default class MainBackground { this.messagingService.send("doneLoggingOut", { expired: expired, userId: userId }); } - await this.setIcon(); - await this.refreshBadgeAndMenu(true); + if (BrowserApi.manifestVersion === 3) { + BrowserApi.sendMessage("updateBadge"); + } + await this.refreshBadge(); + await this.refreshMenu(true); await this.reseedStorage(); this.notificationsService.updateConnection(false); await this.systemService.clearPendingClipboard(); @@ -801,18 +792,15 @@ export default class MainBackground { } private async contextMenuReady(tab: any, contextMenuEnabled: boolean) { - await this.loadMenuAndUpdateBadge(tab.url, tab.id, contextMenuEnabled); + await this.loadMenu(tab.url, tab.id, contextMenuEnabled); this.onUpdatedRan = this.onReplacedRan = false; } - private async loadMenuAndUpdateBadge(url: string, tabId: number, contextMenuEnabled: boolean) { + private async loadMenu(url: string, tabId: number, contextMenuEnabled: boolean) { if (!url || (!chrome.browserAction && !this.sidebarAction)) { return; } - this.actionSetBadgeBackgroundColor(chrome.browserAction); - this.actionSetBadgeBackgroundColor(this.sidebarAction); - this.menuOptionsLoaded = []; const authStatus = await this.authService.getAuthStatus(); if (authStatus === AuthenticationStatus.Unlocked) { @@ -826,50 +814,26 @@ export default class MainBackground { }); } - const disableBadgeCounter = await this.stateService.getDisableBadgeCounter(); - let theText = ""; - - if (!disableBadgeCounter) { - if (ciphers.length > 0 && ciphers.length <= 9) { - theText = ciphers.length.toString(); - } else if (ciphers.length > 0) { - theText = "9+"; - } - } - if (contextMenuEnabled && ciphers.length === 0) { await this.loadNoLoginsContextMenuOptions(this.i18nService.t("noMatchingLogins")); } - this.sidebarActionSetBadgeText(theText, tabId); - this.browserActionSetBadgeText(theText, tabId); - return; } catch (e) { this.logService.error(e); } } - await this.loadMenuAndUpdateBadgeForNoAccessState(contextMenuEnabled); + await this.loadMenuForNoAccessState(contextMenuEnabled); } - private async loadMenuAndUpdateBadgeForNoAccessState(contextMenuEnabled: boolean) { + private async loadMenuForNoAccessState(contextMenuEnabled: boolean) { if (contextMenuEnabled) { const authed = await this.stateService.getIsAuthenticated(); await this.loadNoLoginsContextMenuOptions( this.i18nService.t(authed ? "unlockVaultMenu" : "loginToVaultMenu") ); } - - const tabs = await BrowserApi.getActiveTabs(); - if (tabs != null) { - tabs.forEach((tab) => { - if (tab.id != null) { - this.browserActionSetBadgeText("", tab.id); - this.sidebarActionSetBadgeText("", tab.id); - } - }); - } } private async loadLoginContextMenuOptions(cipher: any) { @@ -1026,42 +990,4 @@ export default class MainBackground { }); } } - - private actionSetBadgeBackgroundColor(action: any) { - if (action && action.setBadgeBackgroundColor) { - action.setBadgeBackgroundColor({ color: "#294e5f" }); - } - } - - private browserActionSetBadgeText(text: string, tabId: number) { - if (chrome.browserAction && chrome.browserAction.setBadgeText) { - chrome.browserAction.setBadgeText({ - text: text, - tabId: tabId, - }); - } - } - - private sidebarActionSetBadgeText(text: string, tabId: number) { - if (!this.sidebarAction) { - return; - } - - if (this.sidebarAction.setBadgeText) { - this.sidebarAction.setBadgeText({ - text: text, - tabId: tabId, - }); - } else if (this.sidebarAction.setTitle) { - let title = "Bitwarden"; - if (text && text !== "") { - title += " [" + text + "]"; - } - - this.sidebarAction.setTitle({ - title: title, - tabId: tabId, - }); - } - } } diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 652996e2525..309edee28ce 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -51,7 +51,7 @@ export default class RuntimeBackground { }; BrowserApi.messageListener("runtime.background", backgroundMessageListener); - if (this.main.isPrivateMode) { + if (this.main.popupOnlyContext) { (window as any).bitwardenBackgroundMessageListener = backgroundMessageListener; } } @@ -71,8 +71,8 @@ export default class RuntimeBackground { } } - await this.main.setIcon(); - await this.main.refreshBadgeAndMenu(false); + await this.main.refreshBadge(); + await this.main.refreshMenu(false); this.notificationsService.updateConnection(msg.command === "unlocked"); this.systemService.cancelProcessReload(); @@ -93,7 +93,10 @@ export default class RuntimeBackground { break; case "syncCompleted": if (msg.successfully) { - setTimeout(async () => await this.main.refreshBadgeAndMenu(), 2000); + setTimeout(async () => { + await this.main.refreshBadge(); + await this.main.refreshMenu(); + }, 2000); } break; case "openPopup": @@ -112,7 +115,8 @@ export default class RuntimeBackground { case "editedCipher": case "addedCipher": case "deletedCipher": - await this.main.refreshBadgeAndMenu(); + await this.main.refreshBadge(); + await this.main.refreshMenu(); break; case "bgReseedStorage": await this.main.reseedStorage(); diff --git a/apps/browser/src/background/service_factories/crypto-function-service.factory.ts b/apps/browser/src/background/service_factories/crypto-function-service.factory.ts index d7611c86a1f..6a092091746 100644 --- a/apps/browser/src/background/service_factories/crypto-function-service.factory.ts +++ b/apps/browser/src/background/service_factories/crypto-function-service.factory.ts @@ -5,7 +5,7 @@ import { CachedServices, factory, FactoryOptions } from "./factory-options"; type CryptoFunctionServiceFactoryOptions = FactoryOptions & { cryptoFunctionServiceOptions: { - win: Window | typeof global; + win: Window | typeof globalThis; }; }; diff --git a/apps/browser/src/background/service_factories/storage-service.factory.ts b/apps/browser/src/background/service_factories/storage-service.factory.ts index 09ee4c7371a..51993277c92 100644 --- a/apps/browser/src/background/service_factories/storage-service.factory.ts +++ b/apps/browser/src/background/service_factories/storage-service.factory.ts @@ -1,6 +1,7 @@ import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service"; +import { BrowserApi } from "../../browser/browserApi"; import BrowserLocalStorageService from "../../services/browserLocalStorage.service"; import { LocalBackedSessionStorageService } from "../../services/localBackedSessionStorage.service"; @@ -38,7 +39,7 @@ export function memoryStorageServiceFactory( opts: MemoryStorageServiceInitOptions ): Promise { return factory(cache, "memoryStorageService", opts, async () => { - if (chrome.runtime.getManifest().manifest_version == 3) { + if (BrowserApi.manifestVersion === 3) { return new LocalBackedSessionStorageService( await encryptServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts) diff --git a/apps/browser/src/background/service_factories/two-factor-service.factory.ts b/apps/browser/src/background/service_factories/two-factor-service.factory.ts index 07bed5400b4..6a0788e8465 100644 --- a/apps/browser/src/background/service_factories/two-factor-service.factory.ts +++ b/apps/browser/src/background/service_factories/two-factor-service.factory.ts @@ -28,6 +28,6 @@ export async function twoFactorServiceFactory( await platformUtilsServiceFactory(cache, opts) ) ); - await service.init(); + service.init(); return service; } diff --git a/apps/browser/src/background/tabs.background.ts b/apps/browser/src/background/tabs.background.ts index ba56d996923..70d2ce1f002 100644 --- a/apps/browser/src/background/tabs.background.ts +++ b/apps/browser/src/background/tabs.background.ts @@ -24,7 +24,8 @@ export default class TabsBackground { }); chrome.tabs.onActivated.addListener(async (activeInfo: chrome.tabs.TabActiveInfo) => { - await this.main.refreshBadgeAndMenu(); + await this.main.refreshBadge(); + await this.main.refreshMenu(); this.main.messagingService.send("tabChanged"); }); @@ -35,7 +36,8 @@ export default class TabsBackground { this.main.onReplacedRan = true; await this.notificationBackground.checkNotificationQueue(); - await this.main.refreshBadgeAndMenu(); + await this.main.refreshBadge(); + await this.main.refreshMenu(); this.main.messagingService.send("tabChanged"); }); @@ -55,7 +57,8 @@ export default class TabsBackground { this.main.onUpdatedRan = true; await this.notificationBackground.checkNotificationQueue(tab); - await this.main.refreshBadgeAndMenu(); + await this.main.refreshBadge(); + await this.main.refreshMenu(); this.main.messagingService.send("tabChanged"); } ); diff --git a/apps/browser/src/background/webRequest.background.ts b/apps/browser/src/background/webRequest.background.ts index 26dd86a9081..b4b4cc4f37c 100644 --- a/apps/browser/src/background/webRequest.background.ts +++ b/apps/browser/src/background/webRequest.background.ts @@ -4,6 +4,8 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus"; import { UriMatchType } from "@bitwarden/common/enums/uriMatchType"; +import { BrowserApi } from "../browser/browserApi"; + export default class WebRequestBackground { private pendingAuthRequests: any[] = []; private webRequest: any; @@ -14,8 +16,7 @@ export default class WebRequestBackground { private cipherService: CipherService, private authService: AuthService ) { - const manifest = chrome.runtime.getManifest(); - if (manifest.manifest_version === 2) { + if (BrowserApi.manifestVersion === 2) { this.webRequest = (window as any).chrome.webRequest; } this.isFirefox = platformUtilsService.isFirefox(); diff --git a/apps/browser/src/browser/browserApi.ts b/apps/browser/src/browser/browserApi.ts index 363adb43750..d64231833df 100644 --- a/apps/browser/src/browser/browserApi.ts +++ b/apps/browser/src/browser/browserApi.ts @@ -1,3 +1,4 @@ +import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service"; import { TabMessage } from "../types/tab-messages"; export class BrowserApi { @@ -10,6 +11,10 @@ export class BrowserApi { static isFirefoxOnAndroid: boolean = navigator.userAgent.indexOf("Firefox/") !== -1 && navigator.userAgent.indexOf("Android") !== -1; + static get manifestVersion() { + return chrome.runtime.getManifest().manifest_version; + } + static async getTabFromCurrentWindowId(): Promise | null { return await BrowserApi.tabsQueryFirst({ active: true, @@ -17,6 +22,13 @@ export class BrowserApi { }); } + static async getTab(tabId: number) { + if (tabId == null) { + return null; + } + return await chrome.tabs.get(tabId); + } + static async getTabFromCurrentWindow(): Promise | null { return await BrowserApi.tabsQueryFirst({ active: true, @@ -211,4 +223,16 @@ export class BrowserApi { chrome.runtime.getPlatformInfo(resolve); }); } + + static getBrowserAction() { + return BrowserApi.manifestVersion === 3 ? chrome.action : chrome.browserAction; + } + + static getSidebarAction(win: Window & typeof globalThis) { + return BrowserPlatformUtilsService.isSafari(win) + ? null + : typeof win.opr !== "undefined" && win.opr.sidebarAction + ? win.opr.sidebarAction + : win.chrome.sidebarAction; + } } diff --git a/apps/browser/src/decorators/session-sync-observable/session-syncer.ts b/apps/browser/src/decorators/session-sync-observable/session-syncer.ts index c757a44c7f2..2acfed2954f 100644 --- a/apps/browser/src/decorators/session-sync-observable/session-syncer.ts +++ b/apps/browser/src/decorators/session-sync-observable/session-syncer.ts @@ -29,7 +29,7 @@ export class SessionSyncer { } init() { - if (chrome.runtime.getManifest().manifest_version != 3) { + if (BrowserApi.manifestVersion !== 3) { return; } diff --git a/apps/browser/src/globals.d.ts b/apps/browser/src/globals.d.ts index 9662c3d71b9..ace24cf3900 100644 --- a/apps/browser/src/globals.d.ts +++ b/apps/browser/src/globals.d.ts @@ -100,28 +100,28 @@ type OperaSidebarAction = { onBlur: OperaEvent; }; +/** + * This is for firefox's sidebar action and it is based on the opera one but with a few less methods + * + * @link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/sidebarAction + */ +type FirefoxSidebarAction = Omit< + OperaSidebarAction, + | "setBadgeText" + | "getBadgeText" + | "setBadgeBackgroundColor" + | "getBadgeBackgroundColor" + | "onFocus" + | "onBlur" +>; + type Opera = { addons: OperaAddons; sidebarAction: OperaSidebarAction; }; declare namespace chrome { - /** - * This is for firefoxes sidebar action and it is based on the opera one but with a few less methods - * - * @link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/sidebarAction - */ - let sidebarAction: - | Omit< - OperaSidebarAction, - | "setBadgeText" - | "getBadgeText" - | "setBadgeBackgroundColor" - | "getBadgeBackgroundColor" - | "onFocus" - | "onBlur" - > - | undefined; + let sidebarAction: FirefoxSidebarAction | undefined; } interface Window { diff --git a/apps/browser/src/listeners/update-badge.ts b/apps/browser/src/listeners/update-badge.ts new file mode 100644 index 00000000000..888f4ce7917 --- /dev/null +++ b/apps/browser/src/listeners/update-badge.ts @@ -0,0 +1,276 @@ +import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service"; +import { AuthService } from "@bitwarden/common/abstractions/auth.service"; +import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; +import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus"; +import { StateFactory } from "@bitwarden/common/factories/stateFactory"; +import { Utils } from "@bitwarden/common/misc/utils"; +import { GlobalState } from "@bitwarden/common/models/domain/global-state"; +import { ContainerService } from "@bitwarden/common/services/container.service"; + +import IconDetails from "../background/models/iconDetails"; +import { authServiceFactory } from "../background/service_factories/auth-service.factory"; +import { cipherServiceFactory } from "../background/service_factories/cipher-service.factory"; +import { searchServiceFactory } from "../background/service_factories/search-service.factory"; +import { stateServiceFactory } from "../background/service_factories/state-service.factory"; +import { BrowserApi } from "../browser/browserApi"; +import { Account } from "../models/account"; +import { StateService } from "../services/abstractions/state.service"; +import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service"; + +export type BadgeOptions = { + tab?: chrome.tabs.Tab; + windowId?: number; +}; + +export class UpdateBadge { + private authService: AuthService; + private stateService: StateService; + private cipherService: CipherService; + private badgeAction: typeof chrome.action; + private sidebarAction: OperaSidebarAction | FirefoxSidebarAction; + private inited = false; + private win: Window & typeof globalThis; + + private static readonly listenedToCommands = [ + "updateBadge", + "loggedIn", + "unlocked", + "syncCompleted", + "bgUpdateContextMenu", + "editedCipher", + "addedCipher", + "deletedCipher", + ]; + + static async tabsOnActivatedListener(activeInfo: chrome.tabs.TabActiveInfo) { + await new UpdateBadge(self).run({ tabId: activeInfo.tabId }); + } + + static async tabsOnReplacedListener(addedTabId: number, removedTabId: number) { + await new UpdateBadge(self).run({ tabId: addedTabId }); + } + + static async tabsOnUpdatedListener(tabId: number) { + await new UpdateBadge(self).run({ tabId }); + } + + static async messageListener( + serviceCache: Record, + message: { command: string; tabId: number } + ) { + if (!UpdateBadge.listenedToCommands.includes(message.command)) { + return; + } + + await new UpdateBadge(self).run(); + } + + constructor(win: Window & typeof globalThis) { + this.badgeAction = BrowserApi.getBrowserAction(); + this.sidebarAction = BrowserApi.getSidebarAction(self); + this.win = win; + } + + async run(opts?: { + tabId?: number; + windowId?: number; + existingServices?: Record; + }): Promise { + await this.initServices(opts?.existingServices); + + const authStatus = await this.authService.getAuthStatus(); + + const tab = await this.getTab(opts?.tabId, opts?.windowId); + const windowId = tab?.windowId; + + await this.setBadgeBackgroundColor(); + + switch (authStatus) { + case AuthenticationStatus.LoggedOut: { + await this.setLoggedOut({ tab, windowId }); + break; + } + case AuthenticationStatus.Locked: { + await this.setLocked({ tab, windowId }); + break; + } + case AuthenticationStatus.Unlocked: { + await this.setUnlocked({ tab, windowId }); + break; + } + } + } + + async setLoggedOut(opts: BadgeOptions): Promise { + await this.setBadgeIcon("_gray", opts?.windowId); + await this.setBadgeText("", opts?.tab?.id); + } + + async setLocked(opts: BadgeOptions) { + await this.setBadgeIcon("_locked", opts?.windowId); + await this.setBadgeText("", opts?.tab?.id); + } + + async setUnlocked(opts: BadgeOptions) { + await this.initServices(); + + await this.setBadgeIcon("", opts?.windowId); + + const disableBadgeCounter = await this.stateService.getDisableBadgeCounter(); + if (disableBadgeCounter) { + return; + } + + const ciphers = await this.cipherService.getAllDecryptedForUrl(opts?.tab?.url); + let countText = ciphers.length == 0 ? "" : ciphers.length.toString(); + if (ciphers.length > 9) { + countText = "9+"; + } + await this.setBadgeText(countText, opts?.tab?.id); + } + + setBadgeBackgroundColor(color = "#294e5f") { + if (this.badgeAction?.setBadgeBackgroundColor) { + this.badgeAction.setBadgeBackgroundColor({ color }); + } + if (this.isOperaSidebar(this.sidebarAction)) { + this.sidebarAction.setBadgeBackgroundColor({ color }); + } + } + + setBadgeText(text: string, tabId?: number) { + this.setActionText(text, tabId); + this.setSideBarText(text, tabId); + } + + async setBadgeIcon(iconSuffix: string, windowId?: number) { + const options: IconDetails = { + path: { + 19: "/images/icon19" + iconSuffix + ".png", + 38: "/images/icon38" + iconSuffix + ".png", + }, + }; + if (BrowserPlatformUtilsService.isFirefox()) { + options.windowId = windowId; + } + + await this.setActionIcon(options); + await this.setSidebarActionIcon(options); + } + + private setActionText(text: string, tabId?: number) { + if (this.badgeAction?.setBadgeText) { + this.badgeAction.setBadgeText({ text, tabId }); + } + } + + private setSideBarText(text: string, tabId?: number) { + if (this.isOperaSidebar(this.sidebarAction)) { + this.sidebarAction.setBadgeText({ text, tabId }); + } else if (this.sidebarAction) { + // Firefox + const title = `Bitwarden${Utils.isNullOrEmpty(text) ? "" : ` [${text}]`}`; + this.sidebarAction.setTitle({ title, tabId }); + } + } + + private async setActionIcon(options: IconDetails) { + if (!this.badgeAction?.setIcon) { + return; + } + + if (this.useSyncApiCalls) { + this.badgeAction.setIcon(options); + } else { + await new Promise((resolve) => this.badgeAction.setIcon(options, () => resolve())); + } + } + + private async setSidebarActionIcon(options: IconDetails) { + if (!this.sidebarAction?.setIcon) { + return; + } + + if (this.useSyncApiCalls) { + this.sidebarAction.setIcon(options); + } else { + await new Promise((resolve) => this.sidebarAction.setIcon(options, () => resolve())); + } + } + + private async getTab(tabId?: number, windowId?: number) { + return ( + (await BrowserApi.getTab(tabId)) ?? + (await BrowserApi.tabsQueryFirst({ active: true, windowId })) ?? + (await BrowserApi.tabsQueryFirst({ active: true, lastFocusedWindow: true })) ?? + (await BrowserApi.tabsQueryFirst({ active: true })) + ); + } + + private get useSyncApiCalls() { + return ( + BrowserPlatformUtilsService.isFirefox() || BrowserPlatformUtilsService.isSafari(this.win) + ); + } + + private async initServices(existingServiceCache?: Record): Promise { + if (this.inited) { + return this; + } + + const serviceCache: Record = existingServiceCache || {}; + const opts = { + cryptoFunctionServiceOptions: { win: self }, + encryptServiceOptions: { logMacFailures: false }, + logServiceOptions: { isDev: false }, + platformUtilsServiceOptions: { + clipboardWriteCallback: (clipboardValue: string, clearMs: number) => + Promise.reject("not implemented"), + biometricCallback: () => Promise.reject("not implemented"), + win: self, + }, + stateServiceOptions: { + stateFactory: new StateFactory(GlobalState, Account), + }, + stateMigrationServiceOptions: { + stateFactory: new StateFactory(GlobalState, Account), + }, + apiServiceOptions: { + logoutCallback: () => Promise.reject("not implemented"), + }, + keyConnectorServiceOptions: { + logoutCallback: () => Promise.reject("not implemented"), + }, + i18nServiceOptions: { + systemLanguage: BrowserApi.getUILanguage(self), + }, + }; + this.stateService = await stateServiceFactory(serviceCache, opts); + this.authService = await authServiceFactory(serviceCache, opts); + const searchService = await searchServiceFactory(serviceCache, opts); + + this.cipherService = await cipherServiceFactory(serviceCache, { + ...opts, + cipherServiceOptions: { searchServiceFactory: () => searchService }, + }); + + // Needed for cipher decryption + if (!self.bitwardenContainerService) { + new ContainerService( + serviceCache.cryptoService as CryptoService, + serviceCache.encryptService as AbstractEncryptService + ).attachToGlobal(self); + } + + this.inited = true; + + return this; + } + + private isOperaSidebar( + action: OperaSidebarAction | FirefoxSidebarAction + ): action is OperaSidebarAction { + return action != null && (action as OperaSidebarAction).setBadgeText != null; + } +} diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index a79028a26d1..da96f7f9037 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -68,13 +68,14 @@ import { PopupSearchService } from "./popup-search.service"; import { PopupUtilsService } from "./popup-utils.service"; import { UnauthGuardService } from "./unauth-guard.service"; -const isPrivateMode = BrowserApi.getBackgroundPage() == null; -const mainBackground: MainBackground = isPrivateMode +const needsBackgroundInit = BrowserApi.getBackgroundPage() == null; +const isPrivateMode = needsBackgroundInit && BrowserApi.manifestVersion !== 3; +const mainBackground: MainBackground = needsBackgroundInit ? createLocalBgService() : BrowserApi.getBackgroundPage().bitwardenMain; function createLocalBgService() { - const localBgService = new MainBackground(true); + const localBgService = new MainBackground(isPrivateMode); localBgService.bootstrap(); return localBgService; } @@ -108,7 +109,7 @@ function getBgService(service: keyof MainBackground) { { provide: MessagingService, useFactory: () => { - return isPrivateMode + return needsBackgroundInit ? new BrowserMessagingPrivateModePopupService() : new BrowserMessagingService(); }, diff --git a/apps/browser/src/services/browserPlatformUtils.service.spec.ts b/apps/browser/src/services/browserPlatformUtils.service.spec.ts index 1f557dc7426..683254087e7 100644 --- a/apps/browser/src/services/browserPlatformUtils.service.spec.ts +++ b/apps/browser/src/services/browserPlatformUtils.service.spec.ts @@ -16,7 +16,7 @@ describe("Browser Utils Service", () => { let browserPlatformUtilsService: BrowserPlatformUtilsService; beforeEach(() => { (window as any).matchMedia = jest.fn().mockReturnValueOnce({}); - browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null, null, self); + browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null, null, window); }); afterEach(() => { diff --git a/apps/browser/src/services/browserPlatformUtils.service.ts b/apps/browser/src/services/browserPlatformUtils.service.ts index 48c305a91e7..feb50a5ccf1 100644 --- a/apps/browser/src/services/browserPlatformUtils.service.ts +++ b/apps/browser/src/services/browserPlatformUtils.service.ts @@ -28,24 +28,17 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService return this.deviceCache; } - if ( - navigator.userAgent.indexOf(" Firefox/") !== -1 || - navigator.userAgent.indexOf(" Gecko/") !== -1 - ) { + if (BrowserPlatformUtilsService.isFirefox()) { this.deviceCache = DeviceType.FirefoxExtension; - } else if ( - (!!this.win.opr && !!opr.addons) || - !!this.win.opera || - navigator.userAgent.indexOf(" OPR/") >= 0 - ) { + } else if (BrowserPlatformUtilsService.isOpera(this.win)) { this.deviceCache = DeviceType.OperaExtension; - } else if (navigator.userAgent.indexOf(" Edg/") !== -1) { + } else if (BrowserPlatformUtilsService.isEdge()) { this.deviceCache = DeviceType.EdgeExtension; - } else if (navigator.userAgent.indexOf(" Vivaldi/") !== -1) { + } else if (BrowserPlatformUtilsService.isVivaldi()) { this.deviceCache = DeviceType.VivaldiExtension; - } else if (this.win.chrome && navigator.userAgent.indexOf(" Chrome/") !== -1) { + } else if (BrowserPlatformUtilsService.isChrome(this.win)) { this.deviceCache = DeviceType.ChromeExtension; - } else if (navigator.userAgent.indexOf(" Safari/") !== -1) { + } else if (BrowserPlatformUtilsService.isSafari(this.win)) { this.deviceCache = DeviceType.SafariExtension; } @@ -61,26 +54,58 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService return ClientType.Browser; } + static isFirefox(): boolean { + return ( + navigator.userAgent.indexOf(" Firefox/") !== -1 || + navigator.userAgent.indexOf(" Gecko/") !== -1 + ); + } + isFirefox(): boolean { return this.getDevice() === DeviceType.FirefoxExtension; } + static isChrome(win: Window & typeof globalThis): boolean { + return win.chrome && navigator.userAgent.indexOf(" Chrome/") !== -1; + } + isChrome(): boolean { return this.getDevice() === DeviceType.ChromeExtension; } + static isEdge(): boolean { + return navigator.userAgent.indexOf(" Edg/") !== -1; + } + isEdge(): boolean { return this.getDevice() === DeviceType.EdgeExtension; } + static isOpera(win: Window & typeof globalThis): boolean { + return ( + (!!win.opr && !!win.opr.addons) || !!win.opera || navigator.userAgent.indexOf(" OPR/") >= 0 + ); + } + isOpera(): boolean { return this.getDevice() === DeviceType.OperaExtension; } + static isVivaldi(): boolean { + return navigator.userAgent.indexOf(" Vivaldi/") !== -1; + } + isVivaldi(): boolean { return this.getDevice() === DeviceType.VivaldiExtension; } + static isSafari(win: Window & typeof globalThis): boolean { + // Opera masquerades as Safari, so make sure we're not there first + return ( + !BrowserPlatformUtilsService.isOpera(win) && navigator.userAgent.indexOf(" Safari/") !== -1 + ); + } + isSafari(): boolean { return this.getDevice() === DeviceType.SafariExtension; }