diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/platform')
3 files changed, 535 insertions, 0 deletions
diff --git a/packages/taler-wallet-webextension/src/platform/api.ts b/packages/taler-wallet-webextension/src/platform/api.ts new file mode 100644 index 000000000..9b4e02ffb --- /dev/null +++ b/packages/taler-wallet-webextension/src/platform/api.ts @@ -0,0 +1,90 @@ +/* + This file is part of TALER + (C) 2017 INRIA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { CoreApiResponse, NotificationType, TalerUriType } from "@gnu-taler/taler-util"; + +export interface Permissions { + /** + * List of named permissions. + */ + permissions?: string[] | undefined; + /** + * List of origin permissions. Anything listed here must be a subset of a + * host that appears in the optional_permissions list in the manifest. + * + */ + origins?: string[] | undefined; + +} + +/** + * Compatibility API that works on multiple browsers. + */ +export interface CrossBrowserPermissionsApi { + contains(p: Permissions): Promise<boolean>; + request(p: Permissions): Promise<boolean>; + remove(p: Permissions): Promise<boolean>; + + addPermissionsListener(callback: (p: Permissions) => void): void; + +} + +export type MessageFromBackend = { + type: NotificationType; +}; + +export interface WalletVersion { + version_name?: string | undefined; + version: string; +} + +/** + * Compatibility helpers needed for browsers that don't implement + * WebExtension APIs consistently. + */ +export interface PlatformAPI { + /** + * check if the platform is firefox + */ + isFirefox(): boolean; + /** + * + */ + getPermissionsApi(): CrossBrowserPermissionsApi; + notifyWhenAppIsReady(callback: () => void): void; + openWalletURIFromPopup(uriType: TalerUriType, talerUri: string): void; + openWalletPage(page: string): void; + openWalletPageFromPopup(page: string): void; + setMessageToWalletBackground(operation: string, payload: any): Promise<CoreApiResponse>; + listenToWalletNotifications(listener: (m: any) => void): () => void; + sendMessageToAllChannels(message: MessageFromBackend): void; + registerAllIncomingConnections(): void; + registerOnNewMessage(onNewMessage: (message: any, sender: any, callback: any) => void): void; + registerReloadOnNewVersion(): void; + redirectTabToWalletPage(tabId: number, page: string): void; + getWalletVersion(): WalletVersion; + registerTalerHeaderListener(onHeader: (tabId: number, url: string) => void): void; + registerOnInstalled(callback: () => void): void; + useServiceWorkerAsBackgroundProcess(): boolean; + getLastError(): string | undefined; + searchForTalerLinks(): string | undefined; + findTalerUriInActiveTab(): Promise<string | undefined>; +} + +export let platform: PlatformAPI = undefined as any; +export function setupPlatform(impl: PlatformAPI) { + platform = impl; +} diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts new file mode 100644 index 000000000..dada23c57 --- /dev/null +++ b/packages/taler-wallet-webextension/src/platform/chrome.ts @@ -0,0 +1,371 @@ +/* + This file is part of TALER + (C) 2017 INRIA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { TalerUriType } from "@gnu-taler/taler-util"; +import { getReadRequestPermissions } from "../permissions"; +import { CrossBrowserPermissionsApi, MessageFromBackend, Permissions, PlatformAPI } from "./api.js"; + +const api: PlatformAPI = { + isFirefox, + findTalerUriInActiveTab, + getLastError, + getPermissionsApi, + getWalletVersion, + listenToWalletNotifications, + notifyWhenAppIsReady, + openWalletPage, + openWalletPageFromPopup, + openWalletURIFromPopup, + redirectTabToWalletPage, + registerAllIncomingConnections, + registerOnInstalled, + registerOnNewMessage, + registerReloadOnNewVersion, + registerTalerHeaderListener, + searchForTalerLinks, + sendMessageToAllChannels, + setMessageToWalletBackground, + useServiceWorkerAsBackgroundProcess +} + +export default api; + +function isFirefox(): boolean { + return false; +} + +export function contains(p: Permissions): Promise<boolean> { + return new Promise((res, rej) => { + chrome.permissions.contains(p, (resp) => { + const le = getLastError() + if (le) { + rej(le) + } + res(resp) + }) + }) +} + +export async function request(p: Permissions): Promise<boolean> { + return new Promise((res, rej) => { + chrome.permissions.request(p, (resp) => { + const le = getLastError() + if (le) { + rej(le) + } + res(resp) + }) + }) +} + +export async function remove(p: Permissions): Promise<boolean> { + return new Promise((res, rej) => { + chrome.permissions.remove(p, (resp) => { + const le = getLastError() + if (le) { + rej(le) + } + res(resp) + }) + }) +} + +function addPermissionsListener(callback: (p: Permissions) => void): void { + console.log("addPermissionListener is not supported for Firefox"); + chrome.permissions.onAdded.addListener(callback) +} + +function getPermissionsApi(): CrossBrowserPermissionsApi { + return { + addPermissionsListener, contains, request, remove + } +} + +/** + * + * @param callback function to be called + */ +function notifyWhenAppIsReady(callback: () => void) { + if (chrome.runtime && chrome.runtime.getManifest().manifest_version === 3) { + callback() + } else { + window.addEventListener("load", callback); + } +} + + +function openWalletURIFromPopup(uriType: TalerUriType, talerUri: string) { + let url: string | undefined = undefined; + switch (uriType) { + case TalerUriType.TalerWithdraw: + 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}`); + break; + case TalerUriType.TalerTip: + 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}`); + break; + default: + console.warn( + "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.", + ); + return; + } + + chrome.tabs.create( + { active: true, url, }, + () => { window.close(); }, + ); +} + +function openWalletPage(page: string) { + const url = chrome.runtime.getURL(`/static/wallet.html#${page}`) + chrome.tabs.create( + { active: true, url, }, + ); +} + +function openWalletPageFromPopup(page: string) { + const url = chrome.runtime.getURL(`/static/wallet.html#${page}`) + chrome.tabs.create( + { active: true, url, }, + () => { window.close(); }, + ); +} + +async function setMessageToWalletBackground(operation: string, payload: any): Promise<any> { + return new Promise<any>((resolve, reject) => { + chrome.runtime.sendMessage({ operation, payload, id: "(none)" }, (resp) => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError.message) + } + resolve(resp) + // return true to keep the channel open + return true; + }) + }) +} + +let notificationPort: chrome.runtime.Port | undefined; +function listenToWalletNotifications(listener: (m: any) => void) { + if (notificationPort === undefined) { + notificationPort = chrome.runtime.connect({ name: "notifications" }) + } + notificationPort.onMessage.addListener(listener) + function removeListener() { + if (notificationPort !== undefined) { + notificationPort.onMessage.removeListener(listener) + } + } + return removeListener +} + + +const allPorts: chrome.runtime.Port[] = []; + +function sendMessageToAllChannels(message: MessageFromBackend) { + for (const notif of allPorts) { + // const message: MessageFromBackend = { type: msg.type }; + try { + notif.postMessage(message); + } catch (e) { + console.error(e); + } + } +} + +function registerAllIncomingConnections() { + chrome.runtime.onConnect.addListener((port) => { + allPorts.push(port); + port.onDisconnect.addListener((discoPort) => { + const idx = allPorts.indexOf(discoPort); + if (idx >= 0) { + allPorts.splice(idx, 1); + } + }); + }); +} + +function registerOnNewMessage(cb: (message: any, sender: any, callback: any) => void) { + chrome.runtime.onMessage.addListener((m, s, c) => { + cb(m, s, c) + + // keep the connection open + return true; + }); +} + +function registerReloadOnNewVersion() { + // Explicitly unload the extension page as soon as an update is available, + // so the update gets installed as soon as possible. + chrome.runtime.onUpdateAvailable.addListener((details) => { + console.log("update available:", details); + chrome.runtime.reload(); + }); + +} + +function redirectTabToWalletPage( + tabId: number, + page: string, +) { + const url = chrome.runtime.getURL(`/static/wallet.html#${page}`); + console.log("redirecting tabId: ", tabId, " to: ", url); + chrome.tabs.update(tabId, { url }); +} + +interface WalletVersion { + version_name?: string | undefined; + version: string; +} + +function getWalletVersion(): WalletVersion { + const manifestData = chrome.runtime.getManifest(); + return manifestData; +} + + +function registerTalerHeaderListener(callback: (tabId: number, url: string) => void): void { + console.log("setting up header listener"); + + function headerListener( + details: chrome.webRequest.WebResponseHeadersDetails, + ) { + if (chrome.runtime.lastError) { + console.error(JSON.stringify(chrome.runtime.lastError)); + return; + } + if ( + details.statusCode === 402 || + details.statusCode === 202 || + details.statusCode === 200 + ) { + const values = (details.responseHeaders || []) + .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]) + } + } + return; + } + + getPermissionsApi().contains(getReadRequestPermissions()).then(result => { + //if there is a handler already, remove it + if ( + "webRequest" in chrome && + "onHeadersReceived" in chrome.webRequest && + chrome.webRequest.onHeadersReceived.hasListener(headerListener) + ) { + chrome.webRequest.onHeadersReceived.removeListener(headerListener); + } + //if the result was positive, add the headerListener + if (result) { + chrome.webRequest.onHeadersReceived.addListener( + headerListener, + { urls: ["<all_urls>"] }, + ["responseHeaders"], + ); + } + //notify the browser about this change, this operation is expensive + if ("webRequest" in chrome) { + chrome.webRequest.handlerBehaviorChanged(() => { + if (chrome.runtime.lastError) { + console.error(JSON.stringify(chrome.runtime.lastError)); + } + }); + } + }); +} + +function registerOnInstalled(callback: () => void) { + // This needs to be outside of main, as Firefox won't fire the event if + // the listener isn't created synchronously on loading the backend. + chrome.runtime.onInstalled.addListener((details) => { + console.log("onInstalled with reason", details.reason); + if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) { + callback() + } + }); +} + +function useServiceWorkerAsBackgroundProcess() { + return chrome.runtime.getManifest().manifest_version === 3 +} + +function getLastError() { + return chrome.runtime.lastError?.message; +} + + +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 +} + +async function getCurrentTab() { + let queryOptions = { active: true, currentWindow: true }; + let [tab] = await chrome.tabs.query(queryOptions); + return tab; +} + + +async function findTalerUriInActiveTab(): Promise<string | undefined> { + if (chrome.runtime.getManifest().manifest_version === 3) { + // manifest v3 + const tab = await getCurrentTab(); + const res = await chrome.scripting.executeScript({ + target: { + tabId: tab.id!, + allFrames: true, + } as any, + func: searchForTalerLinks, + args: [] + }) + return res[0].result + } + return new Promise((resolve, reject) => { + //manifest v2 + chrome.tabs.executeScript( + { + code: ` + (() => { + let x = document.querySelector("a[href^='taler://'") || document.querySelector("a[href^='taler+http://'"); + return x ? x.href.toString() : null; + })(); + `, + allFrames: false, + }, + (result) => { + if (chrome.runtime.lastError) { + console.error(JSON.stringify(chrome.runtime.lastError)); + resolve(undefined); + return; + } + resolve(result[0]); + }, + ); + }); +} diff --git a/packages/taler-wallet-webextension/src/platform/firefox.ts b/packages/taler-wallet-webextension/src/platform/firefox.ts new file mode 100644 index 000000000..dad90626b --- /dev/null +++ b/packages/taler-wallet-webextension/src/platform/firefox.ts @@ -0,0 +1,74 @@ +/* + This file is part of TALER + (C) 2017 INRIA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { CrossBrowserPermissionsApi, Permissions, PlatformAPI } from "./api.js"; +import chromePlatform, { contains as chromeContains, remove as chromeRemove, request as chromeRequest } from "./chrome"; + +const api: PlatformAPI = { + ...chromePlatform, + isFirefox, + getPermissionsApi, + notifyWhenAppIsReady, + redirectTabToWalletPage, + useServiceWorkerAsBackgroundProcess +}; + +export default api; + +function isFirefox(): boolean { + return true +} + + +function addPermissionsListener(callback: (p: Permissions) => void): void { + console.log("addPermissionListener is not supported for Firefox") +} + +function getPermissionsApi(): CrossBrowserPermissionsApi { + return { + addPermissionsListener, + contains: chromeContains, + request: chromeRequest, + remove: chromeRemove + } +} + +/** + * + * @param callback function to be called + */ +function notifyWhenAppIsReady(callback: () => void) { + if (chrome.runtime && chrome.runtime.getManifest().manifest_version === 3) { + callback() + } else { + window.addEventListener("load", callback); + } +} + + +function redirectTabToWalletPage( + tabId: number, + page: string, +) { + const url = chrome.runtime.getURL(`/static/wallet.html#${page}`); + console.log("redirecting tabId: ", tabId, " to: ", url); + chrome.tabs.update(tabId, { url, loadReplace: true } as any); +} + + +function useServiceWorkerAsBackgroundProcess() { + return false +} |