diff options
author | Sebastian <sebasjm@gmail.com> | 2023-04-13 12:19:00 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-04-13 12:57:39 -0300 |
commit | ebd004195673c58718c7c9d8b8270df28b35b539 (patch) | |
tree | a3331d68cb650f2945df82539c7b7a1578bbf0be /packages/taler-wallet-webextension | |
parent | 2baa42f22346be1a1baafb96ba8b5169da8db83c (diff) |
taler wallet interaction support, first version
Diffstat (limited to 'packages/taler-wallet-webextension')
5 files changed, 341 insertions, 1 deletions
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 <http://www.gnu.org/licenses/> + */ + +/** + * 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 <script /> 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 <http://www.gnu.org/licenses/> + */ + +/** + * 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<Info>; + api: API; +} + +function buildApi(config: Readonly<Info>): 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(); |