/* 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 || !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; __internal: 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 : "_self"; const page = convertURIToWebExtensionPath(hrefAttr.value); if (!page) { logger.debug(`onclick: could not convert "${hrefAttr.value}" into path`); return; } // we can use window.open, but maybe some browser will block it? 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() { if (document.body) overrideAllAnchor(document.body) new MutationObserver(checkForNewAnchors).observe(document, { childList: true, subtree: true, attributes: false, }); } 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"; const apiEnabled = searchParams.get("api") === "true"; const hijackEnabled = searchParams.get("hijack") === "true"; const info: Info = Object.freeze({ extensionId, protocol, hostname, }); if (debugEnabled) { logger.debug = logger.info; } const taler: TalerSupport = { info, __internal: buildApi(info), }; if (apiEnabled) { //@ts-ignore window.taler = taler; } if (hijackEnabled) { taler.__internal.registerProtocolHandler(); } } // utils functions function validateTalerUri(uri: string): boolean { return ( !!uri && (uri.startsWith("taler://") || uri.startsWith("taler+http://")) ); } start(); })()