From ebd004195673c58718c7c9d8b8270df28b35b539 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 13 Apr 2023 12:19:00 -0300 Subject: taler wallet interaction support, first version --- .../src/taler-wallet-interaction-support.ts | 192 +++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts (limited to 'packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts') 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(); -- cgit v1.2.3