/*
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;
__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() {
overrideAllAnchor(document.body)
new MutationObserver(checkForNewAnchors).observe(document, {
childList: true,
subtree: true,
attributes: false,
});
}
return {
convertURIToWebExtensionPath,
anchorOnClick,
registerProtocolHandler,
};
}
function start() {
if (shouldNotRun) return;
// FIXME: we can remove this if the script caller send information we need
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,
__internal: buildApi(info),
};
//@ts-ignore
window.taler = taler;
//default behavior: register on install
taler.__internal.registerProtocolHandler();
}
// utils functions
function validateTalerUri(uri: string): boolean {
return (
!!uri && (uri.startsWith("taler://") || uri.startsWith("taler+http://"))
);
}
start();