mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-22 11:45:59 +01:00
Ps 1291 fix extension icon updates (#3571)
* Add needed factories for AuthService WIP: Allow console logs * Add badge updates * Init by listener * Improve tab identification * Define MV3 background init * Init services in factories. Requires conversion of all factories to promises. We need to initialize in factory since the requester of a service doesn't necessarily know all dependencies for that service. The only alternative is to create an out parameter for a generated init function, which isn't ideal. * Improve badge setting * Use `update-badge` in mv2 and mv3 Separates menu and badge updates * Use update-badge everywhere * Use BrowserApi where possible * Update factories * Merge duplicated methods * Continue using private mode messager for now * Add static platform determination. * Break down methods and extract BrowserApi Concerns * Prefer strict equals * Init two-factor service in factory * Use globalThis types * Prefer `globalThis` * Use Window type definition updated with Opera Co-authored-by: Justin Baur <justindbaur@users.noreply.github.com> * Distinguish Opera from Safari Opera includes Gecko, Chrome, Safari, and Opera in its user agent. We need to make sure that we're not in Opera prior to testing Safari. * Update import * Initialize search-service for update badge context * Build all browser MV3 artifacts only uploading Chrome, Edge and Opera artifacts for now, as those support manifest V3 Also corrects build artifact to lower case. * Remove individual dist Co-authored-by: Justin Baur <justindbaur@users.noreply.github.com> Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
This commit is contained in:
parent
23d4dcd839
commit
b4ac5a8bef
@ -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<string, unknown>,
|
||||
message: { command: string }
|
||||
) => void | Promise<void>)[] = [UpdateBadge.messageListener];
|
||||
type AlarmAction = (executionTime: Date, serviceCache: Record<string, unknown>) => 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 = {};
|
||||
|
@ -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<void>((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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -5,7 +5,7 @@ import { CachedServices, factory, FactoryOptions } from "./factory-options";
|
||||
|
||||
type CryptoFunctionServiceFactoryOptions = FactoryOptions & {
|
||||
cryptoFunctionServiceOptions: {
|
||||
win: Window | typeof global;
|
||||
win: Window | typeof globalThis;
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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<AbstractStorageService> {
|
||||
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)
|
||||
|
@ -28,6 +28,6 @@ export async function twoFactorServiceFactory(
|
||||
await platformUtilsServiceFactory(cache, opts)
|
||||
)
|
||||
);
|
||||
await service.init();
|
||||
service.init();
|
||||
return service;
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
);
|
||||
|
@ -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();
|
||||
|
@ -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<chrome.tabs.Tab> | 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<chrome.tabs.Tab> | 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;
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ export class SessionSyncer {
|
||||
}
|
||||
|
||||
init() {
|
||||
if (chrome.runtime.getManifest().manifest_version != 3) {
|
||||
if (BrowserApi.manifestVersion !== 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
32
apps/browser/src/globals.d.ts
vendored
32
apps/browser/src/globals.d.ts
vendored
@ -100,28 +100,28 @@ type OperaSidebarAction = {
|
||||
onBlur: OperaEvent<Window>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
276
apps/browser/src/listeners/update-badge.ts
Normal file
276
apps/browser/src/listeners/update-badge.ts
Normal file
@ -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<string, unknown>,
|
||||
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<string, unknown>;
|
||||
}): Promise<void> {
|
||||
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<void> {
|
||||
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<void>((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<void>((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<string, unknown>): Promise<UpdateBadge> {
|
||||
if (this.inited) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const serviceCache: Record<string, unknown> = 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;
|
||||
}
|
||||
}
|
@ -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<T>(service: keyof MainBackground) {
|
||||
{
|
||||
provide: MessagingService,
|
||||
useFactory: () => {
|
||||
return isPrivateMode
|
||||
return needsBackgroundInit
|
||||
? new BrowserMessagingPrivateModePopupService()
|
||||
: new BrowserMessagingService();
|
||||
},
|
||||
|
@ -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(() => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user