diff --git a/packages/taler-wallet-webextension/build-fast-with-linaria.mjs b/packages/taler-wallet-webextension/build-fast-with-linaria.mjs
index 1232eac98..44e502b6a 100755
--- a/packages/taler-wallet-webextension/build-fast-with-linaria.mjs
+++ b/packages/taler-wallet-webextension/build-fast-with-linaria.mjs
@@ -60,6 +60,8 @@ const entryPoints = [
'src/background.ts',
'src/stories.tsx',
'src/background.dev.ts',
+ 'src/taler-wallet-interaction-loader.ts',
+ 'src/taler-wallet-interaction-support.ts',
'src/browserWorkerEntry.ts'
]
diff --git a/packages/taler-wallet-webextension/manifest-v2.json b/packages/taler-wallet-webextension/manifest-v2.json
index 6adadad98..a5b77168c 100644
--- a/packages/taler-wallet-webextension/manifest-v2.json
+++ b/packages/taler-wallet-webextension/manifest-v2.json
@@ -26,6 +26,11 @@
"https://*/*",
"webRequest"
],
+ "content_scripts": [{
+ "id": "taler-wallet-interaction-support",
+ "matches": ["file://*/*", "http://*/*", "https://*/*"],
+ "js": ["dist/taler-wallet-interaction-loader.js"]
+ }],
"protocol_handlers": [
{
"protocol": "ext+taler",
diff --git a/packages/taler-wallet-webextension/manifest-v3.json b/packages/taler-wallet-webextension/manifest-v3.json
index 4e18125b3..68b3e23ee 100644
--- a/packages/taler-wallet-webextension/manifest-v3.json
+++ b/packages/taler-wallet-webextension/manifest-v3.json
@@ -29,10 +29,16 @@
"optional_permissions": [
"webRequest"
],
+ "content_scripts": [{
+ "id": "taler-wallet-interaction",
+ "matches": ["file://*/*", "http://*/*", "https://*/*"],
+ "js": ["dist/taler-wallet-interaction-loader.js"]
+ }],
"web_accessible_resources": [
{
"resources": [
- "static/wallet.html"
+ "static/wallet.html",
+ "dist/taler-wallet-interaction-support.js"
],
"matches": [
"https://*/*",
diff --git a/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts b/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
new file mode 100644
index 000000000..838b47397
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
@@ -0,0 +1,135 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU 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.
+
+ GNU 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
+ GNU Taler; see the file COPYING. If not, see
+ */
+
+/**
+ * This will modify all the pages that the user load when navigating with Web Extension enabled
+ *
+ * Can't do useful integration since it run in ISOLATED (or equivalent) mode.
+ *
+ * If taler support is expected, it will inject a script which will complete the integration.
+ */
+
+// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Chrome_incompatibilities#content_script_environment
+
+// ISOLATED mode in chromium browsers
+// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#world
+// X-Ray vision in Firefox
+// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts#xray_vision_in_firefox
+
+// *** IMPORTANT ***
+
+// Content script lifecycle during navigation
+// In Firefox: Content scripts remain injected in a web page after the user has navigated away,
+// however, window object properties are destroyed.
+// In Chrome: Content scripts are destroyed when the user navigates away from a web page.
+
+const documentDocTypeIsHTML =
+ window.document.doctype && window.document.doctype.name === "html";
+const suffixIsNotXMLorPDF =
+ !window.location.pathname.endsWith(".xml") &&
+ !window.location.pathname.endsWith(".pdf");
+const rootElementIsHTML =
+ document.documentElement.nodeName &&
+ document.documentElement.nodeName.toLowerCase() === "html";
+const pageAcceptsTalerSupport = document.head.querySelector(
+ "meta[name=taler-support]",
+);
+// safe check, if one of this is true then taler handler is not useful
+// or not expected
+const shouldNotInject =
+ !documentDocTypeIsHTML ||
+ !suffixIsNotXMLorPDF ||
+ // !pageAcceptsTalerSupport || FIXME: removing this before release for testing
+ !rootElementIsHTML;
+const logger = {
+ debug: (...msg: any[]) => {},
+ info: (...msg: any[]) =>
+ console.log(`${new Date().toISOString()} TALER`, ...msg),
+ error: (...msg: any[]) =>
+ console.error(`${new Date().toISOString()} TALER`, ...msg),
+};
+
+function start() {
+ if (shouldNotInject) {
+ return;
+ }
+ const debugEnabled =
+ pageAcceptsTalerSupport?.getAttribute("debug") === "true";
+ if (debugEnabled) {
+ logger.debug = logger.info;
+ }
+ createBridgeWithExtension();
+ logger.debug("bridged created");
+ injectTalerSupportScript(debugEnabled);
+ logger.debug("done");
+}
+
+/**
+ * Create a element that load the support in the page context.
+ * The interaction support script will create the API to send message
+ * that will be received by this loader and be redirected to the extension
+ * using the bridge.
+ */
+function injectTalerSupportScript(debugEnabled: boolean) {
+ const container = document.head || document.documentElement;
+ const scriptTag = document.createElement("script");
+
+ scriptTag.setAttribute("async", "false");
+ const url = new URL(
+ chrome.runtime.getURL("/dist/taler-wallet-interaction-support.js"),
+ );
+ url.searchParams.set("id", chrome.runtime.id);
+ if (debugEnabled) {
+ url.searchParams.set("debug", "true");
+ }
+ scriptTag.src = url.href;
+ try {
+ container.insertBefore(scriptTag, container.children[0]);
+ } catch (e) {
+ logger.info("inserting link handler failed!");
+ logger.error(e);
+ }
+}
+
+/**
+ * Create a bridge connection between the page and the extension.
+ *
+ * Useful for API calls and replies. Not yet supported.
+ */
+function createBridgeWithExtension() {
+ const port = chrome.runtime.connect();
+
+ window.addEventListener(
+ "message",
+ (event) => {
+ logger.debug("message received", event);
+ if (event.source !== window) {
+ return;
+ }
+ if (event.origin !== window.origin) {
+ return;
+ }
+
+ if (event.data.type && event.data.type === "FROM_PAGE") {
+ logger.debug("Content script received: " + event.data.text);
+ port.postMessage(event.data.text);
+ }
+ },
+ false,
+ );
+}
+
+start();
diff --git a/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts b/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts
new file mode 100644
index 000000000..a0ddc40f1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts
@@ -0,0 +1,192 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU 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.
+
+ GNU 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
+ GNU Taler; see the file COPYING. If not, see
+ */
+
+/**
+ * WARNING
+ *
+ * This script will be loaded and run in every page while the
+ * user us navigating. It must be short, simple and safe.
+ */
+
+const logger = {
+ debug: (...msg: any[]) => {},
+ info: (...msg: any[]) =>
+ console.log(`${new Date().toISOString()} TALER`, ...msg),
+ error: (...msg: any[]) =>
+ console.error(`${new Date().toISOString()} TALER`, ...msg),
+};
+
+const documentDocTypeIsHTML =
+ window.document.doctype && window.document.doctype.name === "html";
+const suffixIsNotXMLorPDF =
+ !window.location.pathname.endsWith(".xml") &&
+ !window.location.pathname.endsWith(".pdf");
+const rootElementIsHTML =
+ document.documentElement.nodeName &&
+ document.documentElement.nodeName.toLowerCase() === "html";
+const pageAcceptsTalerSupport = document.head.querySelector(
+ "meta[name=taler-support]",
+);
+
+// this is also checked by the loader
+// but a double check will prevent running and breaking user navigation
+// if loaded from other location
+const shouldNotRun =
+ !documentDocTypeIsHTML ||
+ !suffixIsNotXMLorPDF ||
+ // !pageAcceptsTalerSupport || FIXME: removing this before release for testing
+ !rootElementIsHTML;
+
+interface Info {
+ extensionId: string;
+ protocol: string;
+ hostname: string;
+}
+interface API {
+ convertURIToWebExtensionPath: (uri: string) => string | undefined;
+ anchorOnClick: (ev: MouseEvent) => void;
+ registerProtocolHandler: () => void;
+}
+interface TalerSupport {
+ info: Readonly;
+ api: API;
+}
+
+function buildApi(config: Readonly): API {
+ /**
+ * Takes an anchor href that starts with taler:// and
+ * returns the path to the web-extension page
+ */
+ function convertURIToWebExtensionPath(uri: string): string | undefined {
+ if (!validateTalerUri(uri)) {
+ logger.error(`taler:// URI is invalid: ${uri}`);
+ return undefined;
+ }
+ const host = `${config.protocol}//${config.hostname}`;
+ const path = `static/wallet.html#/taler-uri/${encodeURIComponent(uri)}`;
+ return `${host}/${path}`;
+ }
+
+ function anchorOnClick(ev: MouseEvent) {
+ if (!(ev.currentTarget instanceof Element)) {
+ logger.debug(`onclick: registered in a link that is not an HTML element`);
+ return;
+ }
+ const hrefAttr = ev.currentTarget.attributes.getNamedItem("href");
+ if (!hrefAttr) {
+ logger.debug(`onclick: link didn't have href with taler:// uri`);
+ return;
+ }
+ const targetAttr = ev.currentTarget.attributes.getNamedItem("target");
+ const windowTarget =
+ targetAttr && targetAttr.value ? targetAttr.value : "taler-wallet";
+ const page = convertURIToWebExtensionPath(hrefAttr.value);
+ if (!page) {
+ logger.debug(`onclick: could not convert "${hrefAttr.value}" into path`);
+ return;
+ }
+ window.open(page, windowTarget);
+ ev.preventDefault();
+ ev.stopPropagation();
+ ev.stopImmediatePropagation();
+ return false;
+ }
+
+ function overrideAllAnchor(root: HTMLElement) {
+ const allAnchors = root.querySelectorAll("a[href^=taler]");
+ logger.debug(`registering taler protocol in ${allAnchors.length} links`);
+ allAnchors.forEach((link) => {
+ if (link instanceof HTMLElement) {
+ link.addEventListener("click", anchorOnClick);
+ }
+ });
+ }
+
+ function checkForNewAnchors(
+ mutations: MutationRecord[],
+ observer: MutationObserver,
+ ) {
+ mutations.forEach((mut) => {
+ if (mut.type === "childList") {
+ mut.addedNodes.forEach((added) => {
+ if (added instanceof HTMLElement) {
+ logger.debug(`new element`, added);
+ overrideAllAnchor(added);
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Check of every anchor and observes for new one.
+ * Register the anchor handler when found
+ */
+ function registerProtocolHandler() {
+ const observer = new MutationObserver(checkForNewAnchors);
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true,
+ attributes: false,
+ });
+
+ overrideAllAnchor(document.body);
+ }
+
+ return {
+ convertURIToWebExtensionPath,
+ anchorOnClick,
+ registerProtocolHandler,
+ };
+}
+
+function start() {
+ if (shouldNotRun) return;
+ if (!(document.currentScript instanceof HTMLScriptElement)) return;
+
+ const url = new URL(document.currentScript.src);
+ const { protocol, searchParams, hostname } = url;
+ const extensionId = searchParams.get("id") ?? "";
+ const debugEnabled = searchParams.get("debug") === "true";
+ if (debugEnabled) {
+ logger.debug = logger.info;
+ }
+
+ const info: Info = Object.freeze({
+ extensionId,
+ protocol,
+ hostname,
+ });
+ const taler: TalerSupport = {
+ info,
+ api: buildApi(info),
+ };
+
+ //@ts-ignore
+ window.taler = taler;
+
+ //default behavior: register on install
+ taler.api.registerProtocolHandler();
+}
+
+// utils functions
+function validateTalerUri(uri: string): boolean {
+ return (
+ !!uri && (uri.startsWith("taler://") || uri.startsWith("taler+http://"))
+ );
+}
+
+start();