aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/platform
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2022-03-23 10:50:12 -0300
committerSebastian <sebasjm@gmail.com>2022-03-23 10:58:57 -0300
commit32f6409ac312f31821f791c3a376168289f0e4f4 (patch)
treec77c660bb85cf359faf74b5cddbe95eb0a915c5e /packages/taler-wallet-webextension/src/platform
parentc539d1803c1376cba0831be64866b6d2c1652403 (diff)
downloadwallet-core-32f6409ac312f31821f791c3a376168289f0e4f4.tar.xz
all the browser related code move into one place, making it easy for specific platform code or mocking for testing
Diffstat (limited to 'packages/taler-wallet-webextension/src/platform')
-rw-r--r--packages/taler-wallet-webextension/src/platform/api.ts90
-rw-r--r--packages/taler-wallet-webextension/src/platform/chrome.ts371
-rw-r--r--packages/taler-wallet-webextension/src/platform/firefox.ts74
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
+}