taler wallet interaction support, first version
This commit is contained in:
parent
2baa42f223
commit
ebd0041956
@ -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'
|
||||
]
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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://*/*",
|
||||
|
@ -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();
|
@ -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();
|
Loading…
Reference in New Issue
Block a user