check if the script should be injected

remove header listener
This commit is contained in:
Sebastian 2023-04-18 10:47:53 -03:00
parent b1a0d034fc
commit b34f3568e8
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
16 changed files with 403 additions and 416 deletions

View File

@ -17,8 +17,7 @@
}, },
"permissions": [ "permissions": [
"unlimitedStorage", "unlimitedStorage",
"http://*/*", "storage",
"https://*/*",
"activeTab" "activeTab"
], ],
"web_accessible_resources": [ "web_accessible_resources": [
@ -28,21 +27,31 @@
"dist/taler-wallet-interaction-support.js.map", "dist/taler-wallet-interaction-support.js.map",
"dist/taler-wallet-interaction-support.js" "dist/taler-wallet-interaction-support.js"
], ],
"optional_permissions": [
"http://*/*",
"https://*/*",
"webRequest"
],
"content_scripts": [{ "content_scripts": [{
"id": "taler-wallet-interaction-support", "id": "taler-wallet-interaction-support",
"matches": ["file://*/*", "http://*/*", "https://*/*"], "matches": ["file://*/*", "http://*/*", "https://*/*"],
"js": ["dist/taler-wallet-interaction-loader.js"] "js": ["dist/taler-wallet-interaction-loader.js"]
}], }],
"protocol_handlers": [ "protocol_handlers": [
{
"protocol": "ext+taler+http",
"name": "Taler Wallet WebExtension",
"uriTemplate": "/static/wallet.html#/cta/taler-uri/%s"
},
{
"protocol": "web+taler+http",
"name": "Taler Wallet WebExtension",
"uriTemplate": "/static/wallet.html#/cta/taler-uri/%s"
},
{ {
"protocol": "ext+taler", "protocol": "ext+taler",
"name": "Taler Wallet WebExtension", "name": "Taler Wallet WebExtension",
"uriTemplate": "/static/wallet.html#/cta/withdraw?d=1&talerWithdrawUri=%s" "uriTemplate": "/static/wallet.html#/cta/taler-uri/%s"
},
{
"protocol": "web+taler",
"name": "Taler Wallet WebExtension",
"uriTemplate": "/static/wallet.html#/cta/taler-uri/%s"
} }
], ],
"browser_action": { "browser_action": {

View File

@ -14,6 +14,7 @@
}, },
"permissions": [ "permissions": [
"unlimitedStorage", "unlimitedStorage",
"storage",
"activeTab", "activeTab",
"scripting", "scripting",
"declarativeContent", "declarativeContent",
@ -26,9 +27,6 @@
} }
} }
}, },
"optional_permissions": [
"webRequest"
],
"content_scripts": [{ "content_scripts": [{
"id": "taler-wallet-interaction", "id": "taler-wallet-interaction",
"matches": ["file://*/*", "http://*/*", "https://*/*"], "matches": ["file://*/*", "http://*/*", "https://*/*"],
@ -50,10 +48,6 @@
] ]
} }
], ],
"host_permissions": [
"http://*/*",
"https://*/*"
],
"action": { "action": {
"default_icon": { "default_icon": {
"16": "static/img/taler-logo-16.png", "16": "static/img/taler-logo-16.png",

View File

@ -30,14 +30,8 @@ import { wxMain } from "./wxBackend.js";
console.log("Wallet setup for Dev API"); console.log("Wallet setup for Dev API");
setupPlatform(devAPI); setupPlatform(devAPI);
try { async function start() {
platform.registerOnInstalled(() => { await platform.notifyWhenAppIsReady();
platform.openWalletPage("/welcome"); await wxMain();
});
} catch (e) {
console.error(e);
} }
start();
platform.notifyWhenAppIsReady(() => {
wxMain();
});

View File

@ -43,6 +43,9 @@ if (isFirefox) {
} }
// setGlobalLogLevelFromString("trace") // setGlobalLogLevelFromString("trace")
platform.notifyWhenAppIsReady(() => {
wxMain(); async function start() {
}); await platform.notifyWhenAppIsReady();
await wxMain();
}
start();

View File

@ -20,6 +20,12 @@ import { useBackendContext } from "../context/backend.js";
import { ToggleHandler } from "../mui/handlers.js"; import { ToggleHandler } from "../mui/handlers.js";
import { platform } from "../platform/foreground.js"; import { platform } from "../platform/foreground.js";
/**
* This is not implemented.
* Clipboard permission need to get ask the permission to the user
* based on user-intention
* @returns
*/
export function useClipboardPermissions(): ToggleHandler { export function useClipboardPermissions(): ToggleHandler {
const [enabled, setEnabled] = useState(false); const [enabled, setEnabled] = useState(false);
const api = useBackendContext(); const api = useBackendContext();
@ -40,27 +46,27 @@ export function useClipboardPermissions(): ToggleHandler {
} }
setEnabled(granted); setEnabled(granted);
} else { } else {
try { // try {
await api.background // await api.background
.call("toggleHeaderListener", false) // .call("toggleHeaderListener", false)
.then((r) => setEnabled(r.newValue)); // .then((r) => setEnabled(r.newValue));
} catch (e) { // } catch (e) {
console.log(e); // console.log(e);
} // }
} }
return; return;
} }
useEffect(() => { // useEffect(() => {
async function getValue(): Promise<void> { // async function getValue(): Promise<void> {
const res = await api.background.call( // const res = await api.background.call(
"containsHeaderListener", // "containsHeaderListener",
undefined, // undefined,
); // );
setEnabled(res.newValue); // setEnabled(res.newValue);
} // }
getValue(); // getValue();
}, []); // }, []);
return { return {
value: enabled, value: enabled,

View File

@ -15,14 +15,7 @@
*/ */
import { useLocalStorage } from "@gnu-taler/web-util/lib/index.browser"; import { useLocalStorage } from "@gnu-taler/web-util/lib/index.browser";
import { Settings, defaultSettings } from "../platform/api.js";
interface Settings {
injectTalerSupport: boolean;
}
const defaultSettings: Settings = {
injectTalerSupport: false,
};
function parse_json_or_undefined<T>(str: string | undefined): T | undefined { function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
if (str === undefined) return undefined; if (str === undefined) return undefined;
@ -42,7 +35,6 @@ export function useSettings(): [
const parsed: Settings = parse_json_or_undefined(value) ?? defaultSettings; const parsed: Settings = parse_json_or_undefined(value) ?? defaultSettings;
function updateField<T extends keyof Settings>(k: T, v: Settings[T]) { function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
const newValue = { ...parsed, [k]: v }; const newValue = { ...parsed, [k]: v };
console.log("should update", k, v, parsed, newValue);
const json = JSON.stringify(newValue); const json = JSON.stringify(newValue);
console.log(json); console.log(json);
update(json); update(json);

View File

@ -17,6 +17,10 @@
import { CoreApiResponse, NotificationType } from "@gnu-taler/taler-util"; import { CoreApiResponse, NotificationType } from "@gnu-taler/taler-util";
import { WalletOperations } from "@gnu-taler/taler-wallet-core"; import { WalletOperations } from "@gnu-taler/taler-wallet-core";
import { BackgroundOperations } from "../wxApi.js"; import { BackgroundOperations } from "../wxApi.js";
import {
ExtensionOperations,
MessageFromExtension,
} from "../taler-wallet-interaction-loader.js";
export interface Permissions { export interface Permissions {
/** /**
@ -35,9 +39,9 @@ export interface Permissions {
* Compatibility API that works on multiple browsers. * Compatibility API that works on multiple browsers.
*/ */
export interface CrossBrowserPermissionsApi { export interface CrossBrowserPermissionsApi {
containsHostPermissions(): Promise<boolean>; // containsHostPermissions(): Promise<boolean>;
requestHostPermissions(): Promise<boolean>; // requestHostPermissions(): Promise<boolean>;
removeHostPermissions(): Promise<boolean>; // removeHostPermissions(): Promise<boolean>;
containsClipboardPermissions(): Promise<boolean>; containsClipboardPermissions(): Promise<boolean>;
requestClipboardPermissions(): Promise<boolean>; requestClipboardPermissions(): Promise<boolean>;
@ -53,9 +57,11 @@ export type MessageFromBackend = {
}; };
export type MessageFromFrontend< export type MessageFromFrontend<
Op extends BackgroundOperations | WalletOperations, Op extends BackgroundOperations | WalletOperations | ExtensionOperations,
> = Op extends BackgroundOperations > = Op extends BackgroundOperations
? MessageFromFrontendBackground<keyof BackgroundOperations> ? MessageFromFrontendBackground<keyof BackgroundOperations>
: Op extends ExtensionOperations
? MessageFromExtension<keyof ExtensionOperations>
: Op extends WalletOperations : Op extends WalletOperations
? MessageFromFrontendWallet<keyof WalletOperations> ? MessageFromFrontendWallet<keyof WalletOperations>
: never; : never;
@ -81,11 +87,23 @@ export interface WalletWebExVersion {
version: string; version: string;
} }
export interface Settings {
injectTalerSupport: boolean;
}
export const defaultSettings: Settings = {
injectTalerSupport: false,
};
/** /**
* Compatibility helpers needed for browsers that don't implement * Compatibility helpers needed for browsers that don't implement
* WebExtension APIs consistently. * WebExtension APIs consistently.
*/ */
export interface BackgroundPlatformAPI { export interface BackgroundPlatformAPI {
/**
*
*/
getSettingsFromStorage(): Promise<Settings>;
/** /**
* Guarantee that the service workers don't die * Guarantee that the service workers don't die
*/ */
@ -116,16 +134,12 @@ export interface BackgroundPlatformAPI {
* Register a callback to be called when the wallet is ready to start * Register a callback to be called when the wallet is ready to start
* @param callback * @param callback
*/ */
notifyWhenAppIsReady(callback: () => void): void; notifyWhenAppIsReady(): Promise<void>;
/** /**
* Get the wallet version from manifest * Get the wallet version from manifest
*/ */
getWalletWebExVersion(): WalletWebExVersion; getWalletWebExVersion(): WalletWebExVersion;
/**
* Frontend API
*/
containsTalerHeaderListener(): boolean;
/** /**
* Backend API * Backend API
*/ */
@ -134,12 +148,6 @@ export interface BackgroundPlatformAPI {
* Backend API * Backend API
*/ */
registerReloadOnNewVersion(): void; registerReloadOnNewVersion(): void;
/**
* Backend API
*/
registerTalerHeaderListener(
onHeader: (tabId: number, url: string) => void,
): void;
/** /**
* Permission API for checking and add a listener * Permission API for checking and add a listener

View File

@ -31,10 +31,13 @@ import {
MessageFromFrontend, MessageFromFrontend,
MessageResponse, MessageResponse,
Permissions, Permissions,
Settings,
defaultSettings,
} from "./api.js"; } from "./api.js";
const api: BackgroundPlatformAPI & ForegroundPlatformAPI = { const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
isFirefox, isFirefox,
getSettingsFromStorage,
findTalerUriInActiveTab, findTalerUriInActiveTab,
findTalerUriInClipboard, findTalerUriInClipboard,
getPermissionsApi, getPermissionsApi,
@ -49,11 +52,9 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
registerOnInstalled, registerOnInstalled,
listenToAllChannels: listenToAllChannels as any, listenToAllChannels: listenToAllChannels as any,
registerReloadOnNewVersion, registerReloadOnNewVersion,
registerTalerHeaderListener,
sendMessageToAllChannels, sendMessageToAllChannels,
sendMessageToBackground, sendMessageToBackground,
useServiceWorkerAsBackgroundProcess, useServiceWorkerAsBackgroundProcess,
containsTalerHeaderListener,
keepAlive, keepAlive,
}; };
@ -61,6 +62,19 @@ export default api;
const logger = new Logger("chrome.ts"); const logger = new Logger("chrome.ts");
async function getSettingsFromStorage(): Promise<Settings> {
const data = await chrome.storage.local.get("wallet-settings");
if (!data) return defaultSettings;
const settings = data["wallet-settings"];
if (!settings) return defaultSettings;
try {
const parsed = JSON.parse(settings);
return parsed;
} catch (e) {
return defaultSettings;
}
}
function keepAlive(callback: any): void { function keepAlive(callback: any): void {
if (extensionIsManifestV3()) { if (extensionIsManifestV3()) {
chrome.alarms.create("wallet-worker", { periodInMinutes: 1 }); chrome.alarms.create("wallet-worker", { periodInMinutes: 1 });
@ -78,10 +92,10 @@ function isFirefox(): boolean {
return false; return false;
} }
const hostPermissions = { // const hostPermissions = {
permissions: ["webRequest"], // permissions: ["webRequest"],
origins: ["http://*/*", "https://*/*"], // origins: ["http://*/*", "https://*/*"],
}; // };
export function containsClipboardPermissions(): Promise<boolean> { export function containsClipboardPermissions(): Promise<boolean> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
@ -96,17 +110,17 @@ export function containsClipboardPermissions(): Promise<boolean> {
}); });
} }
export function containsHostPermissions(): Promise<boolean> { // export function containsHostPermissions(): Promise<boolean> {
return new Promise((res, rej) => { // return new Promise((res, rej) => {
chrome.permissions.contains(hostPermissions, (resp) => { // chrome.permissions.contains(hostPermissions, (resp) => {
const le = chrome.runtime.lastError?.message; // const le = chrome.runtime.lastError?.message;
if (le) { // if (le) {
rej(le); // rej(le);
} // }
res(resp); // res(resp);
}); // });
}); // });
} // }
export async function requestClipboardPermissions(): Promise<boolean> { export async function requestClipboardPermissions(): Promise<boolean> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
@ -121,73 +135,67 @@ export async function requestClipboardPermissions(): Promise<boolean> {
}); });
} }
export async function requestHostPermissions(): Promise<boolean> { // export async function requestHostPermissions(): Promise<boolean> {
return new Promise((res, rej) => { // return new Promise((res, rej) => {
chrome.permissions.request(hostPermissions, (resp) => { // chrome.permissions.request(hostPermissions, (resp) => {
const le = chrome.runtime.lastError?.message; // const le = chrome.runtime.lastError?.message;
if (le) { // if (le) {
rej(le); // rej(le);
} // }
res(resp); // res(resp);
}); // });
}); // });
} // }
type HeaderListenerFunc = ( // type HeaderListenerFunc = (
details: chrome.webRequest.WebResponseHeadersDetails, // details: chrome.webRequest.WebResponseHeadersDetails,
) => void; // ) => void;
let currentHeaderListener: HeaderListenerFunc | undefined = undefined; // let currentHeaderListener: HeaderListenerFunc | undefined = undefined;
type TabListenerFunc = (tabId: number, info: chrome.tabs.TabChangeInfo) => void; // type TabListenerFunc = (tabId: number, info: chrome.tabs.TabChangeInfo) => void;
let currentTabListener: TabListenerFunc | undefined = undefined; // let currentTabListener: TabListenerFunc | undefined = undefined;
export function containsTalerHeaderListener(): boolean { // export async function removeHostPermissions(): Promise<boolean> {
return ( // //if there is a handler already, remove it
currentHeaderListener !== undefined || currentTabListener !== undefined // if (
); // currentHeaderListener &&
} // chrome?.webRequest?.onHeadersReceived?.hasListener(currentHeaderListener)
// ) {
// chrome.webRequest.onHeadersReceived.removeListener(currentHeaderListener);
// }
// if (
// currentTabListener &&
// chrome?.tabs?.onUpdated?.hasListener(currentTabListener)
// ) {
// chrome.tabs.onUpdated.removeListener(currentTabListener);
// }
export async function removeHostPermissions(): Promise<boolean> { // currentHeaderListener = undefined;
//if there is a handler already, remove it // currentTabListener = undefined;
if (
currentHeaderListener &&
chrome?.webRequest?.onHeadersReceived?.hasListener(currentHeaderListener)
) {
chrome.webRequest.onHeadersReceived.removeListener(currentHeaderListener);
}
if (
currentTabListener &&
chrome?.tabs?.onUpdated?.hasListener(currentTabListener)
) {
chrome.tabs.onUpdated.removeListener(currentTabListener);
}
currentHeaderListener = undefined; // //notify the browser about this change, this operation is expensive
currentTabListener = undefined; // if ("webRequest" in chrome) {
// chrome.webRequest.handlerBehaviorChanged(() => {
// if (chrome.runtime.lastError) {
// logger.error(JSON.stringify(chrome.runtime.lastError));
// }
// });
// }
//notify the browser about this change, this operation is expensive // if (extensionIsManifestV3()) {
if ("webRequest" in chrome) { // // Trying to remove host permissions with manifest >= v3 throws an error
chrome.webRequest.handlerBehaviorChanged(() => { // return true;
if (chrome.runtime.lastError) { // }
logger.error(JSON.stringify(chrome.runtime.lastError)); // return new Promise((res, rej) => {
} // chrome.permissions.remove(hostPermissions, (resp) => {
}); // const le = chrome.runtime.lastError?.message;
} // if (le) {
// rej(le);
if (extensionIsManifestV3()) { // }
// Trying to remove host permissions with manifest >= v3 throws an error // res(resp);
return true; // });
} // });
return new Promise((res, rej) => { // }
chrome.permissions.remove(hostPermissions, (resp) => {
const le = chrome.runtime.lastError?.message;
if (le) {
rej(le);
}
res(resp);
});
});
}
export function removeClipboardPermissions(): Promise<boolean> { export function removeClipboardPermissions(): Promise<boolean> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
@ -214,9 +222,9 @@ function addPermissionsListener(
function getPermissionsApi(): CrossBrowserPermissionsApi { function getPermissionsApi(): CrossBrowserPermissionsApi {
return { return {
addPermissionsListener, addPermissionsListener,
containsHostPermissions, // containsHostPermissions,
requestHostPermissions, // requestHostPermissions,
removeHostPermissions, // removeHostPermissions,
requestClipboardPermissions, requestClipboardPermissions,
removeClipboardPermissions, removeClipboardPermissions,
containsClipboardPermissions, containsClipboardPermissions,
@ -227,12 +235,16 @@ function getPermissionsApi(): CrossBrowserPermissionsApi {
* *
* @param callback function to be called * @param callback function to be called
*/ */
function notifyWhenAppIsReady(callback: () => void): void { function notifyWhenAppIsReady(): Promise<void> {
return new Promise((resolve, reject) => {
if (extensionIsManifestV3()) { if (extensionIsManifestV3()) {
callback(); resolve();
} else { } else {
window.addEventListener("load", callback); window.addEventListener("load", () => {
resolve();
});
} }
});
} }
function openWalletURIFromPopup(maybeTalerUri: string): void { function openWalletURIFromPopup(maybeTalerUri: string): void {
@ -478,101 +490,6 @@ function getWalletWebExVersion(): WalletVersion {
return manifestData; return manifestData;
} }
function registerTalerHeaderListener(
callback: (tabId: number, url: string) => void,
): void {
logger.trace("setting up header listener");
function headerListener(
details: chrome.webRequest.WebResponseHeadersDetails,
): void {
if (chrome.runtime.lastError) {
logger.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) {
logger.info(
`Found a Taler URI in a response header for the request ${details.url} from tab ${details.tabId}`,
);
callback(details.tabId, values[0]);
}
}
return;
}
async function tabListener(
tabId: number,
info: chrome.tabs.TabChangeInfo,
): Promise<void> {
if (tabId < 0) return;
const tabLocationHasBeenUpdated = info.status === "complete";
const tabTitleHasBeenUpdated = info.title !== undefined;
if (tabLocationHasBeenUpdated || tabTitleHasBeenUpdated) {
const uri = await findTalerUriInTab(tabId);
if (!uri) return;
logger.info(`Found a Taler URI in the tab ${tabId}`);
callback(tabId, uri);
}
}
const prevHeaderListener = currentHeaderListener;
const prevTabListener = currentTabListener;
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 (
prevTabListener &&
chrome?.tabs?.onUpdated?.hasListener(prevTabListener)
) {
chrome.tabs.onUpdated.removeListener(prevTabListener);
}
//if the result was positive, add the headerListener
if (result) {
const headersEvent:
| chrome.webRequest.WebResponseHeadersEvent
| undefined = chrome?.webRequest?.onHeadersReceived;
if (headersEvent) {
headersEvent.addListener(headerListener, { urls: ["<all_urls>"] }, [
"responseHeaders",
]);
currentHeaderListener = headerListener;
}
const tabsEvent: chrome.tabs.TabUpdatedEvent | undefined =
chrome?.tabs?.onUpdated;
if (tabsEvent) {
tabsEvent.addListener(tabListener);
currentTabListener = tabListener;
}
}
//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 = { const alertIcons = {
"16": "/static/img/taler-alert-16.png", "16": "/static/img/taler-alert-16.png",
"19": "/static/img/taler-alert-19.png", "19": "/static/img/taler-alert-19.png",
@ -750,7 +667,7 @@ function registerOnInstalled(callback: () => void): void {
if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) { if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) {
callback(); callback();
} }
registerIconChangeOnTalerContent(); await registerIconChangeOnTalerContent();
}); });
} }

View File

@ -23,18 +23,17 @@ import {
MessageFromBackend, MessageFromBackend,
MessageFromFrontend, MessageFromFrontend,
MessageResponse, MessageResponse,
defaultSettings,
} from "./api.js"; } from "./api.js";
const frames = ["popup", "wallet"]; const frames = ["popup", "wallet"];
const api: BackgroundPlatformAPI & ForegroundPlatformAPI = { const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
isFirefox: () => false, isFirefox: () => false,
getSettingsFromStorage: () => Promise.resolve(defaultSettings),
keepAlive: (cb: VoidFunction) => cb(), keepAlive: (cb: VoidFunction) => cb(),
findTalerUriInActiveTab: async () => undefined, findTalerUriInActiveTab: async () => undefined,
findTalerUriInClipboard: async () => undefined, findTalerUriInClipboard: async () => undefined,
containsTalerHeaderListener: () => {
return true;
},
getPermissionsApi: () => ({ getPermissionsApi: () => ({
addPermissionsListener: () => undefined, addPermissionsListener: () => undefined,
containsHostPermissions: async () => true, containsHostPermissions: async () => true,
@ -47,8 +46,9 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
getWalletWebExVersion: () => ({ getWalletWebExVersion: () => ({
version: "none", version: "none",
}), }),
notifyWhenAppIsReady: (fn: () => void) => { notifyWhenAppIsReady: () => {
let total = frames.length; let total = frames.length;
return new Promise((fn) => {
function waitAndNotify(): void { function waitAndNotify(): void {
total--; total--;
if (total < 1) { if (total < 1) {
@ -63,6 +63,7 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
theFrame.addEventListener("load", waitAndNotify); theFrame.addEventListener("load", waitAndNotify);
} }
}); });
});
}, },
openWalletPage: (page: string) => { openWalletPage: (page: string) => {
@ -80,9 +81,8 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
}, },
registerAllIncomingConnections: () => undefined, registerAllIncomingConnections: () => undefined,
registerOnInstalled: (fn: () => void) => undefined, registerOnInstalled: () => Promise.resolve(),
registerReloadOnNewVersion: () => undefined, registerReloadOnNewVersion: () => undefined,
registerTalerHeaderListener: () => undefined,
useServiceWorkerAsBackgroundProcess: () => false, useServiceWorkerAsBackgroundProcess: () => false,

View File

@ -19,11 +19,10 @@ import {
CrossBrowserPermissionsApi, CrossBrowserPermissionsApi,
ForegroundPlatformAPI, ForegroundPlatformAPI,
Permissions, Permissions,
Settings,
defaultSettings,
} from "./api.js"; } from "./api.js";
import chromePlatform, { import chromePlatform, {
containsHostPermissions as chromeHostContains,
removeHostPermissions as chromeHostRemove,
requestHostPermissions as chromeHostRequest,
containsClipboardPermissions as chromeClipContains, containsClipboardPermissions as chromeClipContains,
removeClipboardPermissions as chromeClipRemove, removeClipboardPermissions as chromeClipRemove,
requestClipboardPermissions as chromeClipRequest, requestClipboardPermissions as chromeClipRequest,
@ -32,6 +31,7 @@ import chromePlatform, {
const api: BackgroundPlatformAPI & ForegroundPlatformAPI = { const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
...chromePlatform, ...chromePlatform,
isFirefox, isFirefox,
getSettingsFromStorage,
getPermissionsApi, getPermissionsApi,
notifyWhenAppIsReady, notifyWhenAppIsReady,
redirectTabToWalletPage, redirectTabToWalletPage,
@ -51,25 +51,43 @@ function addPermissionsListener(callback: (p: Permissions) => void): void {
function getPermissionsApi(): CrossBrowserPermissionsApi { function getPermissionsApi(): CrossBrowserPermissionsApi {
return { return {
addPermissionsListener, addPermissionsListener,
containsHostPermissions: chromeHostContains, // containsHostPermissions: chromeHostContains,
requestHostPermissions: chromeHostRequest, // requestHostPermissions: chromeHostRequest,
removeHostPermissions: chromeHostRemove, // removeHostPermissions: chromeHostRemove,
containsClipboardPermissions: chromeClipContains, containsClipboardPermissions: chromeClipContains,
removeClipboardPermissions: chromeClipRemove, removeClipboardPermissions: chromeClipRemove,
requestClipboardPermissions: chromeClipRequest, requestClipboardPermissions: chromeClipRequest,
}; };
} }
async function getSettingsFromStorage(): Promise<Settings> {
//@ts-ignore
const data = await browser.storage.local.get("wallet-settings");
if (!data) return defaultSettings;
const settings = data["wallet-settings"];
if (!settings) return defaultSettings;
try {
const parsed = JSON.parse(settings);
return parsed;
} catch (e) {
return defaultSettings;
}
}
/** /**
* *
* @param callback function to be called * @param callback function to be called
*/ */
function notifyWhenAppIsReady(callback: () => void): void { function notifyWhenAppIsReady(): Promise<void> {
return new Promise((resolve) => {
if (chrome.runtime && chrome.runtime.getManifest().manifest_version === 3) { if (chrome.runtime && chrome.runtime.getManifest().manifest_version === 3) {
callback(); resolve();
} else { } else {
window.addEventListener("load", callback); window.addEventListener("load", () => {
resolve();
});
} }
});
} }
function redirectTabToWalletPage(tabId: number, page: string): void { function redirectTabToWalletPage(tabId: number, page: string): void {

View File

@ -14,6 +14,8 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { CoreApiResponse } from "@gnu-taler/taler-util";
/** /**
* This will modify all the pages that the user load when navigating with Web Extension enabled * This will modify all the pages that the user load when navigating with Web Extension enabled
* *
@ -62,7 +64,7 @@ const logger = {
console.error(`${new Date().toISOString()} TALER`, ...msg), console.error(`${new Date().toISOString()} TALER`, ...msg),
}; };
function start() { async function start() {
if (shouldNotInject) { if (shouldNotInject) {
return; return;
} }
@ -73,8 +75,15 @@ function start() {
} }
createBridgeWithExtension(); createBridgeWithExtension();
logger.debug("bridged created"); logger.debug("bridged created");
const shouldInject = await callBackground("isInjectionEnabled", undefined);
if (shouldInject) {
injectTalerSupportScript(debugEnabled); injectTalerSupportScript(debugEnabled);
logger.debug("done"); logger.debug("injection completed");
} else {
logger.debug("injection is not enabled");
}
} }
/** /**
@ -132,4 +141,65 @@ function createBridgeWithExtension() {
); );
} }
export interface ExtensionOperations {
isInjectionEnabled: {
request: void;
response: boolean;
};
}
export type MessageFromExtension<Op extends keyof ExtensionOperations> = {
channel: "extension";
operation: Op;
payload: ExtensionOperations[Op]["request"];
};
export type MessageResponse = CoreApiResponse;
async function callBackground<Op extends keyof ExtensionOperations>(
operation: Op,
payload: ExtensionOperations[Op]["request"],
): Promise<ExtensionOperations[Op]["response"]> {
const message: MessageFromExtension<Op> = {
channel: "extension",
operation,
payload,
};
const response = await sendMessageToBackground(message);
if (response.type === "error") {
throw new Error(`Background operation "${operation}" failed`);
}
return response.result as any;
}
let nextMessageIndex = 0;
async function sendMessageToBackground<Op extends keyof ExtensionOperations>(
message: MessageFromExtension<Op>,
): Promise<MessageResponse> {
const messageWithId = { ...message, id: `id_${nextMessageIndex++ % 1000}` };
return new Promise<any>((resolve, reject) => {
// logger.trace("send operation to the wallet background", message);
let timedout = false;
const timerId = setTimeout(() => {
timedout = true;
throw new Error("timeout");
// throw TalerError.fromDetail(TalerErrorCode.GENERIC_TIMEOUT, {});
}, 5 * 1000); //five seconds
chrome.runtime.sendMessage(messageWithId, (backgroundResponse) => {
if (timedout) {
return false; //already rejected
}
clearTimeout(timerId);
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError.message);
} else {
resolve(backgroundResponse);
}
// return true to keep the channel open
return true;
});
});
}
start(); start();

View File

@ -92,7 +92,7 @@ function buildApi(config: Readonly<Info>): API {
} }
const targetAttr = ev.currentTarget.attributes.getNamedItem("target"); const targetAttr = ev.currentTarget.attributes.getNamedItem("target");
const windowTarget = const windowTarget =
targetAttr && targetAttr.value ? targetAttr.value : "taler-wallet"; targetAttr && targetAttr.value ? targetAttr.value : "_self";
const page = convertURIToWebExtensionPath(hrefAttr.value); const page = convertURIToWebExtensionPath(hrefAttr.value);
if (!page) { if (!page) {
logger.debug(`onclick: could not convert "${hrefAttr.value}" into path`); logger.debug(`onclick: could not convert "${hrefAttr.value}" into path`);

View File

@ -271,7 +271,7 @@ export function SettingsView({
<i18n.Translate>Navigator</i18n.Translate> <i18n.Translate>Navigator</i18n.Translate>
</SubTitle> </SubTitle>
<Checkbox <Checkbox
label={i18n.str`Automatically inject Taler API in all pages`} label={i18n.str`Inject Taler support in all pages`}
name="inject" name="inject"
description={ description={
<i18n.Translate> <i18n.Translate>

View File

@ -100,7 +100,7 @@ export function View({
<i18n.Translate>Navigator</i18n.Translate> <i18n.Translate>Navigator</i18n.Translate>
</SubTitle> </SubTitle>
<Checkbox <Checkbox
label={i18n.str`Automatically inject Taler API in all pages`} label={i18n.str`Inject Taler support in all pages`}
name="inject" name="inject"
description={ description={
<i18n.Translate> <i18n.Translate>

View File

@ -68,18 +68,10 @@ export interface BackgroundOperations {
request: void; request: void;
response: void; response: void;
}; };
containsHeaderListener: {
request: void;
response: ExtendedPermissionsResponse;
};
getDiagnostics: { getDiagnostics: {
request: void; request: void;
response: WalletDiagnostics; response: WalletDiagnostics;
}; };
toggleHeaderListener: {
request: boolean;
response: ExtendedPermissionsResponse;
};
runGarbageCollector: { runGarbageCollector: {
request: void; request: void;
response: void; response: void;

View File

@ -24,42 +24,39 @@
* Imports. * Imports.
*/ */
import { import {
classifyTalerUri,
Logger,
LogLevel, LogLevel,
Logger,
TalerErrorCode,
WalletDiagnostics,
getErrorDetailFromException,
makeErrorDetail,
setGlobalLogLevelFromString, setGlobalLogLevelFromString,
setLogLevelFromString, setLogLevelFromString,
TalerErrorCode,
TalerUriType,
WalletDiagnostics,
makeErrorDetail,
getErrorDetailFromException,
parseTalerUri,
TalerUriAction,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
DbAccess, DbAccess,
deleteTalerDatabase,
exportDb,
importDb,
OpenedPromise, OpenedPromise,
openPromise,
openTalerDatabase,
SetTimeoutTimerAPI, SetTimeoutTimerAPI,
Wallet, Wallet,
WalletOperations, WalletOperations,
WalletStoresV1, WalletStoresV1,
deleteTalerDatabase,
exportDb,
importDb,
openPromise,
openTalerDatabase,
} from "@gnu-taler/taler-wallet-core"; } from "@gnu-taler/taler-wallet-core";
import { BrowserHttpLib } from "./browserHttpLib.js"; import { BrowserHttpLib } from "./browserHttpLib.js";
import { platform } from "./platform/background.js";
import { import {
MessageFromBackend, MessageFromBackend,
MessageFromFrontend, MessageFromFrontend,
MessageResponse, MessageResponse,
} from "./platform/api.js"; } from "./platform/api.js";
import { platform } from "./platform/background.js";
import { SynchronousCryptoWorkerFactory } from "./serviceWorkerCryptoWorkerFactory.js"; import { SynchronousCryptoWorkerFactory } from "./serviceWorkerCryptoWorkerFactory.js";
import { ServiceWorkerHttpLib } from "./serviceWorkerHttpLib.js"; import { ServiceWorkerHttpLib } from "./serviceWorkerHttpLib.js";
import { BackgroundOperations, ExtendedPermissionsResponse } from "./wxApi.js"; import { ExtensionOperations } from "./taler-wallet-interaction-loader.js";
import { BackgroundOperations } from "./wxApi.js";
/** /**
* Currently active wallet instance. Might be unloaded and * Currently active wallet instance. Might be unloaded and
@ -123,10 +120,11 @@ type BackendHandlerType = {
) => Promise<BackgroundOperations[Op]["response"]>; ) => Promise<BackgroundOperations[Op]["response"]>;
}; };
async function containsHeaderListener(): Promise<ExtendedPermissionsResponse> { type ExtensionHandlerType = {
const result = await platform.containsTalerHeaderListener(); [Op in keyof ExtensionOperations]: (
return { newValue: result }; req: ExtensionOperations[Op]["request"],
} ) => Promise<ExtensionOperations[Op]["response"]>;
};
async function resetDb(): Promise<void> { async function resetDb(): Promise<void> {
await deleteTalerDatabase(indexedDB as any); await deleteTalerDatabase(indexedDB as any);
@ -153,20 +151,6 @@ async function runGarbageCollector(): Promise<void> {
logger.info("imported"); logger.info("imported");
} }
async function toggleHeaderListener(
newVal: boolean,
): Promise<ExtendedPermissionsResponse> {
logger.trace("new extended permissions value", newVal);
if (newVal) {
platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
return { newValue: true };
}
const rem = await platform.getPermissionsApi().removeHostPermissions();
logger.trace("permissions removed:", rem);
return { newValue: false };
}
function freeze(time: number): Promise<void> { function freeze(time: number): Promise<void> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
setTimeout(res, time); setTimeout(res, time);
@ -177,14 +161,21 @@ async function sum(ns: Array<number>): Promise<number> {
return ns.reduce((prev, cur) => prev + cur, 0); return ns.reduce((prev, cur) => prev + cur, 0);
} }
const extensionHandlers: ExtensionHandlerType = {
isInjectionEnabled,
};
async function isInjectionEnabled(): Promise<boolean> {
const settings = await platform.getSettingsFromStorage();
return settings.injectTalerSupport === true;
}
const backendHandlers: BackendHandlerType = { const backendHandlers: BackendHandlerType = {
freeze, freeze,
sum, sum,
containsHeaderListener,
getDiagnostics, getDiagnostics,
resetDb, resetDb,
runGarbageCollector, runGarbageCollector,
toggleHeaderListener,
setLoggingLevel, setLoggingLevel,
}; };
@ -203,10 +194,11 @@ async function setLoggingLevel({
} }
} }
async function dispatch<Op extends WalletOperations | BackgroundOperations>( async function dispatch<
req: MessageFromFrontend<Op> & { id: string }, Op extends WalletOperations | BackgroundOperations | ExtensionOperations,
): Promise<MessageResponse> { >(req: MessageFromFrontend<Op> & { id: string }): Promise<MessageResponse> {
if (req.channel === "background") { switch (req.channel) {
case "background": {
const handler = backendHandlers[req.operation] as (req: any) => any; const handler = backendHandlers[req.operation] as (req: any) => any;
if (!handler) { if (!handler) {
return { return {
@ -235,8 +227,36 @@ async function dispatch<Op extends WalletOperations | BackgroundOperations>(
}; };
} }
} }
case "extension": {
if (req.channel === "wallet") { const handler = extensionHandlers[req.operation] as (req: any) => any;
if (!handler) {
return {
type: "error",
id: req.id,
operation: String(req.operation),
error: getErrorDetailFromException(
Error(`unknown extension operation`),
),
};
}
try {
const result = await handler(req.payload);
return {
type: "response",
id: req.id,
operation: String(req.operation),
result,
};
} catch (er) {
return {
type: "error",
id: req.id,
error: getErrorDetailFromException(er),
operation: String(req.operation),
};
}
}
case "wallet": {
const w = currentWallet; const w = currentWallet;
if (!w) { if (!w) {
return { return {
@ -253,6 +273,7 @@ async function dispatch<Op extends WalletOperations | BackgroundOperations>(
return await w.handleCoreApiRequest(req.operation, req.id, req.payload); return await w.handleCoreApiRequest(req.operation, req.id, req.payload);
} }
}
const anyReq = req as any; const anyReq = req as any;
return { return {
@ -261,7 +282,7 @@ async function dispatch<Op extends WalletOperations | BackgroundOperations>(
operation: String(anyReq.operation), operation: String(anyReq.operation),
error: getErrorDetailFromException( error: getErrorDetailFromException(
Error( Error(
`unknown channel ${anyReq.channel}, should be "background" or "wallet"`, `unknown channel ${anyReq.channel}, should be "background", "extension" or "wallet"`,
), ),
), ),
}; };
@ -330,23 +351,6 @@ async function reinitWallet(): Promise<void> {
return walletInit.resolve(); return walletInit.resolve();
} }
function parseTalerUriAndRedirect(tabId: number, maybeTalerUri: string): void {
const talerUri = maybeTalerUri.startsWith("ext+")
? maybeTalerUri.substring(4)
: maybeTalerUri;
const uri = parseTalerUri(talerUri);
if (!uri) {
logger.warn(
`Response with HTTP 402 the Taler header but could not classify ${talerUri}`,
);
return;
}
return platform.redirectTabToWalletPage(
tabId,
`/taler-uri/${encodeURIComponent(talerUri)}`,
);
}
/** /**
* Main function to run for the WebExtension backend. * Main function to run for the WebExtension backend.
* *
@ -370,30 +374,10 @@ export async function wxMain(): Promise<void> {
platform.registerAllIncomingConnections(); platform.registerAllIncomingConnections();
try { try {
platform.registerOnInstalled(() => { await platform.registerOnInstalled(() => {
platform.openWalletPage("/welcome"); platform.openWalletPage("/welcome");
//
try {
platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
} catch (e) {
logger.error("could not register header listener", e);
}
}); });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
// On platforms that support it, also listen to external
// modification of permissions.
platform.getPermissionsApi().addPermissionsListener((perm, lastError) => {
if (lastError) {
logger.error(
`there was a problem trying to get permission ${perm}`,
lastError,
);
return;
}
platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
});
} }