bitwarden-estensione-browser/apps/browser/src/platform/browser/browser-api.ts

281 lines
8.4 KiB
TypeScript

import { DeviceType } from "@bitwarden/common/enums";
import { TabMessage } from "../../types/tab-messages";
import BrowserPlatformUtilsService from "../services/browser-platform-utils.service";
export class BrowserApi {
static isWebExtensionsApi: boolean = typeof browser !== "undefined";
static isSafariApi: boolean =
navigator.userAgent.indexOf(" Safari/") !== -1 &&
navigator.userAgent.indexOf(" Chrome/") === -1 &&
navigator.userAgent.indexOf(" Chromium/") === -1;
static isChromeApi: boolean = !BrowserApi.isSafariApi && typeof chrome !== "undefined";
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,
windowId: chrome.windows.WINDOW_ID_CURRENT,
});
}
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,
currentWindow: true,
});
}
static async getActiveTabs(): Promise<chrome.tabs.Tab[]> {
return await BrowserApi.tabsQuery({
active: true,
});
}
static async tabsQuery(options: chrome.tabs.QueryInfo): Promise<chrome.tabs.Tab[]> {
return new Promise((resolve) => {
chrome.tabs.query(options, (tabs) => {
resolve(tabs);
});
});
}
static async tabsQueryFirst(options: chrome.tabs.QueryInfo): Promise<chrome.tabs.Tab> | null {
const tabs = await BrowserApi.tabsQuery(options);
if (tabs.length > 0) {
return tabs[0];
}
return null;
}
static tabSendMessageData(
tab: chrome.tabs.Tab,
command: string,
data: any = null
): Promise<void> {
const obj: any = {
command: command,
};
if (data != null) {
obj.data = data;
}
return BrowserApi.tabSendMessage(tab, obj);
}
static async tabSendMessage<T>(
tab: chrome.tabs.Tab,
obj: T,
options: chrome.tabs.MessageSendOptions = null
): Promise<void> {
if (!tab || !tab.id) {
return;
}
return new Promise<void>((resolve) => {
chrome.tabs.sendMessage(tab.id, obj, options, () => {
if (chrome.runtime.lastError) {
// Some error happened
}
resolve();
});
});
}
static sendTabsMessage<T>(
tabId: number,
message: TabMessage,
options?: chrome.tabs.MessageSendOptions,
responseCallback?: (response: T) => void
) {
chrome.tabs.sendMessage<TabMessage, T>(tabId, message, options, responseCallback);
}
static async getPrivateModeWindows(): Promise<browser.windows.Window[]> {
return (await browser.windows.getAll()).filter((win) => win.incognito);
}
static async onWindowCreated(callback: (win: chrome.windows.Window) => any) {
return chrome.windows.onCreated.addListener(callback);
}
static getBackgroundPage(): any {
return chrome.extension.getBackgroundPage();
}
static isBackgroundPage(window: Window & typeof globalThis): boolean {
return window === chrome.extension.getBackgroundPage();
}
static getApplicationVersion(): string {
return chrome.runtime.getManifest().version;
}
static async isPopupOpen(): Promise<boolean> {
return Promise.resolve(chrome.extension.getViews({ type: "popup" }).length > 0);
}
static createNewTab(url: string, active = true): Promise<chrome.tabs.Tab> {
return new Promise((resolve) =>
chrome.tabs.create({ url: url, active: active }, (tab) => resolve(tab))
);
}
static async focusWindow(windowId: number) {
await chrome.windows.update(windowId, { focused: true });
}
static async openBitwardenExtensionTab(relativeUrl: string, active = true) {
let url = relativeUrl;
if (!relativeUrl.includes("uilocation=tab")) {
const fullUrl = chrome.extension.getURL(relativeUrl);
const parsedUrl = new URL(fullUrl);
parsedUrl.searchParams.set("uilocation", "tab");
url = parsedUrl.toString();
}
const createdTab = await this.createNewTab(url, active);
this.focusWindow(createdTab.windowId);
}
static async closeBitwardenExtensionTab() {
const tabs = await BrowserApi.tabsQuery({
active: true,
title: "Bitwarden",
windowType: "normal",
currentWindow: true,
});
if (tabs.length === 0) {
return;
}
const tabToClose = tabs[tabs.length - 1];
chrome.tabs.remove(tabToClose.id);
}
private static registeredMessageListeners: any[] = [];
static messageListener(
name: string,
callback: (message: any, sender: chrome.runtime.MessageSender, response: any) => void
) {
chrome.runtime.onMessage.addListener(callback);
// Keep track of all the events registered in a Safari popup so we can remove
// them when the popup gets unloaded, otherwise we cause a memory leak
if (BrowserApi.isSafariApi && !BrowserApi.isBackgroundPage(window)) {
BrowserApi.registeredMessageListeners.push(callback);
// The MDN recommend using 'visibilitychange' but that event is fired any time the popup window is obscured as well
// 'pagehide' works just like 'unload' but is compatible with the back/forward cache, so we prefer using that one
window.onpagehide = () => {
for (const callback of BrowserApi.registeredMessageListeners) {
chrome.runtime.onMessage.removeListener(callback);
}
};
}
}
static sendMessage(subscriber: string, arg: any = {}) {
const message = Object.assign({}, { command: subscriber }, arg);
return chrome.runtime.sendMessage(message);
}
static async focusTab(tabId: number) {
chrome.tabs.update(tabId, { active: true, highlighted: true });
}
static closePopup(win: Window) {
if (BrowserApi.isWebExtensionsApi && BrowserApi.isFirefoxOnAndroid) {
// Reactivating the active tab dismisses the popup tab. The promise final
// condition is only called if the popup wasn't already dismissed (future proofing).
// ref: https://bugzilla.mozilla.org/show_bug.cgi?id=1433604
browser.tabs.update({ active: true }).finally(win.close);
} else {
win.close();
}
}
static gaFilter() {
return process.env.ENV !== "production";
}
static getUILanguage(win: Window) {
return chrome.i18n.getUILanguage();
}
static reloadExtension(win: Window) {
if (win != null) {
return (win.location as any).reload(true);
} else {
return chrome.runtime.reload();
}
}
static reloadOpenWindows(exemptCurrentHref = false) {
const currentHref = window.location.href;
const views = chrome.extension.getViews() as Window[];
views
.filter((w) => w.location.href != null && !w.location.href.includes("background.html"))
.filter((w) => !exemptCurrentHref || w.location.href !== currentHref)
.forEach((w) => {
w.location.reload();
});
}
static connectNative(application: string): browser.runtime.Port | chrome.runtime.Port {
if (BrowserApi.isWebExtensionsApi) {
return browser.runtime.connectNative(application);
} else if (BrowserApi.isChromeApi) {
return chrome.runtime.connectNative(application);
}
}
static requestPermission(permission: any) {
if (BrowserApi.isWebExtensionsApi) {
return browser.permissions.request(permission);
}
return new Promise((resolve, reject) => {
chrome.permissions.request(permission, resolve);
});
}
static getPlatformInfo(): Promise<browser.runtime.PlatformInfo | chrome.runtime.PlatformInfo> {
if (BrowserApi.isWebExtensionsApi) {
return browser.runtime.getPlatformInfo();
}
return new Promise((resolve) => {
chrome.runtime.getPlatformInfo(resolve);
});
}
static getBrowserAction() {
return BrowserApi.manifestVersion === 3 ? chrome.action : chrome.browserAction;
}
static getSidebarAction(
win: Window & typeof globalThis
): OperaSidebarAction | FirefoxSidebarAction | null {
const deviceType = BrowserPlatformUtilsService.getDevice(win);
if (deviceType !== DeviceType.FirefoxExtension && deviceType !== DeviceType.OperaExtension) {
return null;
}
return win.opr?.sidebarAction || browser.sidebarAction;
}
}