2023-04-13 17:19:00 +02:00
|
|
|
/*
|
|
|
|
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>;
|
2023-04-13 20:04:31 +02:00
|
|
|
__internal: API;
|
2023-04-13 17:19:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2023-04-13 17:57:23 +02:00
|
|
|
// we can use window.open, but maybe some browser will block it?
|
2023-04-13 17:19:00 +02:00
|
|
|
window.open(page, windowTarget);
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
ev.stopImmediatePropagation();
|
2023-04-13 17:57:23 +02:00
|
|
|
// another possibility is to change the location when the click is made
|
|
|
|
// or when the anchor is found
|
|
|
|
// hrefAttr.value = page
|
|
|
|
// TODO: explore different possibilities and maybe allow the configuration
|
|
|
|
// using the meta-tag
|
2023-04-13 17:19:00 +02:00
|
|
|
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;
|
2023-04-13 20:04:31 +02:00
|
|
|
// FIXME: we can remove this if the script caller send information we need
|
2023-04-13 17:19:00 +02:00
|
|
|
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,
|
2023-04-13 20:04:31 +02:00
|
|
|
__internal: buildApi(info),
|
2023-04-13 17:19:00 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
//@ts-ignore
|
|
|
|
window.taler = taler;
|
|
|
|
|
|
|
|
//default behavior: register on install
|
2023-04-13 20:04:31 +02:00
|
|
|
taler.__internal.registerProtocolHandler();
|
2023-04-13 17:19:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// utils functions
|
|
|
|
function validateTalerUri(uri: string): boolean {
|
|
|
|
return (
|
|
|
|
!!uri && (uri.startsWith("taler://") || uri.startsWith("taler+http://"))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
start();
|