diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/platform/chrome.ts')
-rw-r--r-- | packages/taler-wallet-webextension/src/platform/chrome.ts | 397 |
1 files changed, 228 insertions, 169 deletions
diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts index e097baaa1..9caf42a58 100644 --- a/packages/taler-wallet-webextension/src/platform/chrome.ts +++ b/packages/taler-wallet-webextension/src/platform/chrome.ts @@ -15,8 +15,18 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { classifyTalerUri, CoreApiResponse, Logger, TalerUriType } from "@gnu-taler/taler-util"; -import { CrossBrowserPermissionsApi, MessageFromBackend, Permissions, PlatformAPI } from "./api.js"; +import { + classifyTalerUri, + CoreApiResponse, + Logger, + TalerUriType, +} from "@gnu-taler/taler-util"; +import { + CrossBrowserPermissionsApi, + MessageFromBackend, + Permissions, + PlatformAPI, +} from "./api.js"; const api: PlatformAPI = { isFirefox, @@ -39,7 +49,7 @@ const api: PlatformAPI = { useServiceWorkerAsBackgroundProcess, containsTalerHeaderListener, keepAlive, -} +}; export default api; @@ -47,16 +57,15 @@ const logger = new Logger("chrome.ts"); function keepAlive(callback: any): void { if (extensionIsManifestV3()) { - chrome.alarms.create("wallet-worker", { periodInMinutes: 1 }) + chrome.alarms.create("wallet-worker", { periodInMinutes: 1 }); chrome.alarms.onAlarm.addListener((a) => { - logger.trace(`kee p alive alarm: ${a.name}`) + logger.trace(`kee p alive alarm: ${a.name}`); // callback() - }) + }); // } else { } callback(); - } function isFirefox(): boolean { @@ -66,34 +75,35 @@ function isFirefox(): boolean { const hostPermissions = { permissions: ["webRequest"], origins: ["http://*/*", "https://*/*"], -} - +}; export function containsHostPermissions(): Promise<boolean> { return new Promise((res, rej) => { chrome.permissions.contains(hostPermissions, (resp) => { - const le = chrome.runtime.lastError?.message + const le = chrome.runtime.lastError?.message; if (le) { - rej(le) + rej(le); } - res(resp) - }) - }) + res(resp); + }); + }); } export async function requestHostPermissions(): Promise<boolean> { return new Promise((res, rej) => { chrome.permissions.request(hostPermissions, (resp) => { - const le = chrome.runtime.lastError?.message + const le = chrome.runtime.lastError?.message; if (le) { - rej(le) + rej(le); } - res(resp) - }) - }) + res(resp); + }); + }); } -type HeaderListenerFunc = (details: chrome.webRequest.WebResponseHeadersDetails) => void +type HeaderListenerFunc = ( + details: chrome.webRequest.WebResponseHeadersDetails, +) => void; let currentHeaderListener: HeaderListenerFunc | undefined = undefined; export function containsTalerHeaderListener(): boolean { @@ -128,57 +138,69 @@ export async function removeHostPermissions(): Promise<boolean> { } return new Promise((res, rej) => { chrome.permissions.remove(hostPermissions, (resp) => { - const le = chrome.runtime.lastError?.message + const le = chrome.runtime.lastError?.message; if (le) { - rej(le) + rej(le); } - res(resp) - }) - }) + res(resp); + }); + }); } -function addPermissionsListener(callback: (p: Permissions, lastError?: string) => void): void { +function addPermissionsListener( + callback: (p: Permissions, lastError?: string) => void, +): void { chrome.permissions.onAdded.addListener((perm: Permissions) => { const lastError = chrome.runtime.lastError?.message; - callback(perm, lastError) - }) + callback(perm, lastError); + }); } function getPermissionsApi(): CrossBrowserPermissionsApi { return { - addPermissionsListener, containsHostPermissions, requestHostPermissions, removeHostPermissions - } + addPermissionsListener, + containsHostPermissions, + requestHostPermissions, + removeHostPermissions, + }; } /** - * + * * @param callback function to be called */ function notifyWhenAppIsReady(callback: () => void): void { if (extensionIsManifestV3()) { - callback() + callback(); } else { window.addEventListener("load", callback); } } - function openWalletURIFromPopup(talerUri: string): void { const uriType = classifyTalerUri(talerUri); let url: string | undefined = undefined; switch (uriType) { case TalerUriType.TalerWithdraw: - url = chrome.runtime.getURL(`static/wallet.html#/cta/withdraw?talerWithdrawUri=${talerUri}`); + url = chrome.runtime.getURL( + `static/wallet.html#/cta/withdraw?talerWithdrawUri=${talerUri}`, + ); break; case TalerUriType.TalerPay: - url = chrome.runtime.getURL(`static/wallet.html#/cta/pay?talerPayUri=${talerUri}`); + url = chrome.runtime.getURL( + `static/wallet.html#/cta/pay?talerPayUri=${talerUri}`, + ); break; case TalerUriType.TalerTip: - url = chrome.runtime.getURL(`static/wallet.html#/cta/tip?talerTipUri=${talerUri}`); + url = chrome.runtime.getURL( + `static/wallet.html#/cta/tip?talerTipUri=${talerUri}`, + ); break; case TalerUriType.TalerRefund: - url = chrome.runtime.getURL(`static/wallet.html#/cta/refund?talerRefundUri=${talerUri}`); + url = chrome.runtime.getURL( + `static/wallet.html#/cta/refund?talerRefundUri=${talerUri}`, + ); break; default: logger.warn( @@ -187,56 +209,54 @@ function openWalletURIFromPopup(talerUri: string): void { return; } - chrome.tabs.create( - { active: true, url, }, - () => { window.close(); }, - ); + chrome.tabs.create({ active: true, url }, () => { + window.close(); + }); } function openWalletPage(page: string): void { - const url = chrome.runtime.getURL(`/static/wallet.html#${page}`) - chrome.tabs.create( - { active: true, url, }, - ); + const url = chrome.runtime.getURL(`/static/wallet.html#${page}`); + chrome.tabs.create({ active: true, url }); } function openWalletPageFromPopup(page: string): void { - const url = chrome.runtime.getURL(`/static/wallet.html#${page}`) - chrome.tabs.create( - { active: true, url, }, - () => { window.close(); }, - ); + const url = chrome.runtime.getURL(`/static/wallet.html#${page}`); + chrome.tabs.create({ active: true, url }, () => { + window.close(); + }); } -async function sendMessageToWalletBackground(operation: string, payload: any): Promise<any> { +async function sendMessageToWalletBackground( + operation: string, + payload: any, +): Promise<any> { return new Promise<any>((resolve, reject) => { - logger.trace("send operation to the wallet background", operation) + logger.trace("send operation to the wallet background", operation); chrome.runtime.sendMessage({ operation, payload, id: "(none)" }, (resp) => { if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError.message) + reject(chrome.runtime.lastError.message); } - resolve(resp) + resolve(resp); // return true to keep the channel open return true; - }) - }) + }); + }); } let notificationPort: chrome.runtime.Port | undefined; function listenToWalletBackground(listener: (m: any) => void): () => void { if (notificationPort === undefined) { - notificationPort = chrome.runtime.connect({ name: "notifications" }) + notificationPort = chrome.runtime.connect({ name: "notifications" }); } - notificationPort.onMessage.addListener(listener) + notificationPort.onMessage.addListener(listener); function removeListener(): void { if (notificationPort !== undefined) { - notificationPort.onMessage.removeListener(listener) + notificationPort.onMessage.removeListener(listener); } } - return removeListener + return removeListener; } - const allPorts: chrome.runtime.Port[] = []; function sendMessageToAllChannels(message: MessageFromBackend): void { @@ -262,9 +282,15 @@ function registerAllIncomingConnections(): void { }); } -function listenToAllChannels(cb: (message: any, sender: any, callback: (r: CoreApiResponse) => void) => void): void { +function listenToAllChannels( + cb: ( + message: any, + sender: any, + callback: (r: CoreApiResponse) => void, + ) => void, +): void { chrome.runtime.onMessage.addListener((m, s, c) => { - cb(m, s, c) + cb(m, s, c); // keep the connection open return true; @@ -278,13 +304,9 @@ function registerReloadOnNewVersion(): void { logger.info("update available:", details); chrome.runtime.reload(); }); - } -function redirectTabToWalletPage( - tabId: number, - page: string, -): void { +function redirectTabToWalletPage(tabId: number, page: string): void { const url = chrome.runtime.getURL(`/static/wallet.html#${page}`); logger.trace("redirecting tabId: ", tabId, " to: ", url); chrome.tabs.update(tabId, { url }); @@ -300,7 +322,9 @@ function getWalletVersion(): WalletVersion { return manifestData; } -function registerTalerHeaderListener(callback: (tabId: number, url: string) => void): void { +function registerTalerHeaderListener( + callback: (tabId: number, url: string) => void, +): void { logger.trace("setting up header listener"); function headerListener( @@ -316,44 +340,45 @@ function registerTalerHeaderListener(callback: (tabId: number, url: string) => v details.statusCode === 200 ) { const values = (details.responseHeaders || []) - .filter(h => h.name.toLowerCase() === 'taler') - .map(h => h.value) - .filter((value): value is string => !!value) + .filter((h) => h.name.toLowerCase() === "taler") + .map((h) => h.value) + .filter((value): value is string => !!value); if (values.length > 0) { - callback(details.tabId, values[0]) + callback(details.tabId, values[0]); } } return; } const prevHeaderListener = currentHeaderListener; - getPermissionsApi().containsHostPermissions().then(result => { - //if there is a handler already, remove it - if ( - prevHeaderListener && - chrome?.webRequest?.onHeadersReceived?.hasListener(prevHeaderListener) - ) { - chrome.webRequest.onHeadersReceived.removeListener(prevHeaderListener); - } - //if the result was positive, add the headerListener - if (result) { - const listener: chrome.webRequest.WebResponseHeadersEvent | undefined = chrome?.webRequest?.onHeadersReceived; - if (listener) { - listener.addListener( - headerListener, - { urls: ["<all_urls>"] }, - ["responseHeaders"], - ); - currentHeaderListener = headerListener; + getPermissionsApi() + .containsHostPermissions() + .then((result) => { + //if there is a handler already, remove it + if ( + prevHeaderListener && + chrome?.webRequest?.onHeadersReceived?.hasListener(prevHeaderListener) + ) { + chrome.webRequest.onHeadersReceived.removeListener(prevHeaderListener); } - } - //notify the browser about this change, this operation is expensive - chrome?.webRequest?.handlerBehaviorChanged(() => { - if (chrome.runtime.lastError) { - logger.error(JSON.stringify(chrome.runtime.lastError)); + //if the result was positive, add the headerListener + if (result) { + const listener: chrome.webRequest.WebResponseHeadersEvent | undefined = + chrome?.webRequest?.onHeadersReceived; + if (listener) { + listener.addListener(headerListener, { urls: ["<all_urls>"] }, [ + "responseHeaders", + ]); + currentHeaderListener = headerListener; + } } + //notify the browser about this change, this operation is expensive + chrome?.webRequest?.handlerBehaviorChanged(() => { + if (chrome.runtime.lastError) { + logger.error(JSON.stringify(chrome.runtime.lastError)); + } + }); }); - }); } const alertIcons = { @@ -365,8 +390,8 @@ const alertIcons = { "64": "/static/img/taler-alert-64.png", "128": "/static/img/taler-alert-128.png", "256": "/static/img/taler-alert-256.png", - "512": "/static/img/taler-alert-512.png" -} + "512": "/static/img/taler-alert-512.png", +}; const normalIcons = { "16": "/static/img/taler-logo-16.png", "19": "/static/img/taler-logo-19.png", @@ -376,70 +401,99 @@ const normalIcons = { "64": "/static/img/taler-logo-64.png", "128": "/static/img/taler-logo-128.png", "256": "/static/img/taler-logo-256.png", - "512": "/static/img/taler-logo-512.png" -} + "512": "/static/img/taler-logo-512.png", +}; function setNormalIcon(): void { if (extensionIsManifestV3()) { - chrome.action.setIcon({ path: normalIcons }) + chrome.action.setIcon({ path: normalIcons }); } else { - chrome.browserAction.setIcon({ path: normalIcons }) + chrome.browserAction.setIcon({ path: normalIcons }); } } function setAlertedIcon(): void { if (extensionIsManifestV3()) { - chrome.action.setIcon({ path: alertIcons }) + chrome.action.setIcon({ path: alertIcons }); } else { - chrome.browserAction.setIcon({ path: alertIcons }) + chrome.browserAction.setIcon({ path: alertIcons }); } } - -interface OffscreenCanvasRenderingContext2D extends CanvasState, CanvasTransform, CanvasCompositing, CanvasImageSmoothing, CanvasFillStrokeStyles, CanvasShadowStyles, CanvasFilters, CanvasRect, CanvasDrawPath, CanvasUserInterface, CanvasText, CanvasDrawImage, CanvasImageData, CanvasPathDrawingStyles, CanvasTextDrawingStyles, CanvasPath { +interface OffscreenCanvasRenderingContext2D + extends CanvasState, + CanvasTransform, + CanvasCompositing, + CanvasImageSmoothing, + CanvasFillStrokeStyles, + CanvasShadowStyles, + CanvasFilters, + CanvasRect, + CanvasDrawPath, + CanvasUserInterface, + CanvasText, + CanvasDrawImage, + CanvasImageData, + CanvasPathDrawingStyles, + CanvasTextDrawingStyles, + CanvasPath { readonly canvas: OffscreenCanvas; } declare const OffscreenCanvasRenderingContext2D: { prototype: OffscreenCanvasRenderingContext2D; - new(): OffscreenCanvasRenderingContext2D; -} + new (): OffscreenCanvasRenderingContext2D; +}; interface OffscreenCanvas extends EventTarget { width: number; height: number; - getContext(contextId: "2d", contextAttributes?: CanvasRenderingContext2DSettings): OffscreenCanvasRenderingContext2D | null; + getContext( + contextId: "2d", + contextAttributes?: CanvasRenderingContext2DSettings, + ): OffscreenCanvasRenderingContext2D | null; } declare const OffscreenCanvas: { prototype: OffscreenCanvas; - new(width: number, height: number): OffscreenCanvas; -} + new (width: number, height: number): OffscreenCanvas; +}; function createCanvas(size: number): OffscreenCanvas { if (extensionIsManifestV3()) { - return new OffscreenCanvas(size, size) + return new OffscreenCanvas(size, size); } else { - const c = document.createElement("canvas") + const c = document.createElement("canvas"); c.height = size; c.width = size; return c; } } - async function createImage(size: number, file: string): Promise<ImageData> { - const r = await fetch(file) - const b = await r.blob() - const image = await createImageBitmap(b) + const r = await fetch(file); + const b = await r.blob(); + const image = await createImageBitmap(b); const canvas = createCanvas(size); - const canvasContext = canvas.getContext('2d')!; + const canvasContext = canvas.getContext("2d")!; canvasContext.clearRect(0, 0, canvas.width, canvas.height); canvasContext.drawImage(image, 0, 0, canvas.width, canvas.height); - const imageData = canvasContext.getImageData(0, 0, canvas.width, canvas.height); + const imageData = canvasContext.getImageData( + 0, + 0, + canvas.width, + canvas.height, + ); return imageData; } async function registerIconChangeOnTalerContent(): Promise<void> { - const imgs = await Promise.all(Object.entries(alertIcons).map(([key, value]) => createImage(parseInt(key, 10), value))) - const imageData = imgs.reduce((prev, cur) => ({ ...prev, [cur.width]: cur }), {} as { [size: string]: ImageData }) + const imgs = await Promise.all( + Object.entries(alertIcons).map(([key, value]) => + createImage(parseInt(key, 10), value), + ), + ); + const imageData = imgs.reduce( + (prev, cur) => ({ ...prev, [cur.width]: cur }), + {} as { [size: string]: ImageData }, + ); if (chrome.declarativeContent) { // using declarative content does not need host permission @@ -447,49 +501,54 @@ async function registerIconChangeOnTalerContent(): Promise<void> { const secureTalerUrlLookup = { conditions: [ new chrome.declarativeContent.PageStateMatcher({ - css: ["a[href^='taler://'"] - }) + css: ["a[href^='taler://'"], + }), ], - actions: [new chrome.declarativeContent.SetIcon({ imageData })] + actions: [new chrome.declarativeContent.SetIcon({ imageData })], }; const inSecureTalerUrlLookup = { conditions: [ new chrome.declarativeContent.PageStateMatcher({ - css: ["a[href^='taler+http://'"] - }) + css: ["a[href^='taler+http://'"], + }), ], - actions: [new chrome.declarativeContent.SetIcon({ imageData })] + actions: [new chrome.declarativeContent.SetIcon({ imageData })], }; chrome.declarativeContent.onPageChanged.removeRules(undefined, function () { - chrome.declarativeContent.onPageChanged.addRules([secureTalerUrlLookup, inSecureTalerUrlLookup]); + chrome.declarativeContent.onPageChanged.addRules([ + secureTalerUrlLookup, + inSecureTalerUrlLookup, + ]); }); return; } //this browser doesn't have declarativeContent //we need host_permission and we will check the content for changing the icon - chrome.tabs.onUpdated.addListener(async (tabId, info: chrome.tabs.TabChangeInfo) => { - if (tabId < 0) return; - logger.info("tab updated", tabId, info); - if (info.status !== "complete") return; - const uri = await findTalerUriInTab(tabId); - if (uri) { - setAlertedIcon() - } else { - setNormalIcon() - } - - }); - chrome.tabs.onActivated.addListener(async ({ tabId }: chrome.tabs.TabActiveInfo) => { - if (tabId < 0) return; - const uri = await findTalerUriInTab(tabId); - if (uri) { - setAlertedIcon() - } else { - setNormalIcon() - } - }) - + chrome.tabs.onUpdated.addListener( + async (tabId, info: chrome.tabs.TabChangeInfo) => { + if (tabId < 0) return; + logger.info("tab updated", tabId, info); + if (info.status !== "complete") return; + const uri = await findTalerUriInTab(tabId); + if (uri) { + setAlertedIcon(); + } else { + setNormalIcon(); + } + }, + ); + chrome.tabs.onActivated.addListener( + async ({ tabId }: chrome.tabs.TabActiveInfo) => { + if (tabId < 0) return; + const uri = await findTalerUriInTab(tabId); + if (uri) { + setAlertedIcon(); + } else { + setNormalIcon(); + } + }, + ); } function registerOnInstalled(callback: () => void): void { @@ -498,27 +557,27 @@ function registerOnInstalled(callback: () => void): void { chrome.runtime.onInstalled.addListener(async (details) => { logger.info(`onInstalled with reason: "${details.reason}"`); if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) { - callback() + callback(); } - registerIconChangeOnTalerContent() + registerIconChangeOnTalerContent(); }); } function extensionIsManifestV3(): boolean { - return chrome.runtime.getManifest().manifest_version === 3 + return chrome.runtime.getManifest().manifest_version === 3; } function useServiceWorkerAsBackgroundProcess(): boolean { - return extensionIsManifestV3() + return extensionIsManifestV3(); } function searchForTalerLinks(): string | undefined { let found; - found = document.querySelector("a[href^='taler://'") - if (found) return found.toString() - found = document.querySelector("a[href^='taler+http://'") - if (found) return found.toString() - return undefined + found = document.querySelector("a[href^='taler://'"); + if (found) return found.toString(); + found = document.querySelector("a[href^='taler+http://'"); + if (found) return found.toString(); + return undefined; } async function getCurrentTab(): Promise<chrome.tabs.Tab> { @@ -526,12 +585,12 @@ async function getCurrentTab(): Promise<chrome.tabs.Tab> { return new Promise<chrome.tabs.Tab>((resolve, reject) => { chrome.tabs.query(queryOptions, (tabs) => { if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError) + reject(chrome.runtime.lastError); return; } - resolve(tabs[0]) + resolve(tabs[0]); }); - }) + }); } async function findTalerUriInTab(tabId: number): Promise<string | undefined> { @@ -541,16 +600,17 @@ async function findTalerUriInTab(tabId: number): Promise<string | undefined> { const res = await chrome.scripting.executeScript({ target: { tabId, allFrames: true }, func: searchForTalerLinks, - args: [] - }) - return res[0].result + args: [], + }); + return res[0].result; } catch (e) { return; } } else { return new Promise((resolve, reject) => { //manifest v2 - chrome.tabs.executeScript(tabId, + chrome.tabs.executeScript( + tabId, { code: ` (() => { @@ -576,6 +636,5 @@ async function findTalerUriInTab(tabId: number): Promise<string | undefined> { async function findTalerUriInActiveTab(): Promise<string | undefined> { const tab = await getCurrentTab(); if (!tab || tab.id === undefined) return; - return findTalerUriInTab(tab.id) + return findTalerUriInTab(tab.id); } - |