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/>
|
|
|
|
*/
|
|
|
|
|
2023-04-18 15:47:53 +02:00
|
|
|
import { CoreApiResponse } from "@gnu-taler/taler-util";
|
|
|
|
|
2023-04-13 17:19:00 +02:00
|
|
|
/**
|
|
|
|
* 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),
|
|
|
|
};
|
|
|
|
|
2023-04-18 15:47:53 +02:00
|
|
|
async function start() {
|
2023-04-13 17:19:00 +02:00
|
|
|
if (shouldNotInject) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const debugEnabled =
|
|
|
|
pageAcceptsTalerSupport?.getAttribute("debug") === "true";
|
|
|
|
if (debugEnabled) {
|
|
|
|
logger.debug = logger.info;
|
|
|
|
}
|
|
|
|
createBridgeWithExtension();
|
|
|
|
logger.debug("bridged created");
|
2023-04-18 15:47:53 +02:00
|
|
|
|
|
|
|
const shouldInject = await callBackground("isInjectionEnabled", undefined);
|
|
|
|
|
|
|
|
if (shouldInject) {
|
|
|
|
injectTalerSupportScript(debugEnabled);
|
|
|
|
logger.debug("injection completed");
|
|
|
|
} else {
|
|
|
|
logger.debug("injection is not enabled");
|
|
|
|
}
|
2023-04-13 17:19:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-04-18 15:47:53 +02:00
|
|
|
export interface ExtensionOperations {
|
|
|
|
isInjectionEnabled: {
|
|
|
|
request: void;
|
|
|
|
response: boolean;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export type MessageFromExtension<Op extends keyof ExtensionOperations> = {
|
|
|
|
channel: "extension";
|
|
|
|
operation: Op;
|
|
|
|
payload: ExtensionOperations[Op]["request"];
|
|
|
|
};
|
|
|
|
|
|
|
|
export type MessageResponse = CoreApiResponse;
|
|
|
|
|
|
|
|
async function callBackground<Op extends keyof ExtensionOperations>(
|
|
|
|
operation: Op,
|
|
|
|
payload: ExtensionOperations[Op]["request"],
|
|
|
|
): Promise<ExtensionOperations[Op]["response"]> {
|
|
|
|
const message: MessageFromExtension<Op> = {
|
|
|
|
channel: "extension",
|
|
|
|
operation,
|
|
|
|
payload,
|
|
|
|
};
|
|
|
|
|
|
|
|
const response = await sendMessageToBackground(message);
|
|
|
|
if (response.type === "error") {
|
|
|
|
throw new Error(`Background operation "${operation}" failed`);
|
|
|
|
}
|
|
|
|
return response.result as any;
|
|
|
|
}
|
|
|
|
let nextMessageIndex = 0;
|
|
|
|
async function sendMessageToBackground<Op extends keyof ExtensionOperations>(
|
|
|
|
message: MessageFromExtension<Op>,
|
|
|
|
): Promise<MessageResponse> {
|
|
|
|
const messageWithId = { ...message, id: `id_${nextMessageIndex++ % 1000}` };
|
|
|
|
|
|
|
|
return new Promise<any>((resolve, reject) => {
|
|
|
|
// logger.trace("send operation to the wallet background", message);
|
|
|
|
let timedout = false;
|
|
|
|
const timerId = setTimeout(() => {
|
|
|
|
timedout = true;
|
|
|
|
throw new Error("timeout");
|
|
|
|
// throw TalerError.fromDetail(TalerErrorCode.GENERIC_TIMEOUT, {});
|
|
|
|
}, 5 * 1000); //five seconds
|
|
|
|
chrome.runtime.sendMessage(messageWithId, (backgroundResponse) => {
|
|
|
|
if (timedout) {
|
|
|
|
return false; //already rejected
|
|
|
|
}
|
|
|
|
clearTimeout(timerId);
|
|
|
|
if (chrome.runtime.lastError) {
|
|
|
|
reject(chrome.runtime.lastError.message);
|
|
|
|
} else {
|
|
|
|
resolve(backgroundResponse);
|
|
|
|
}
|
|
|
|
// return true to keep the channel open
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-04-13 17:19:00 +02:00
|
|
|
start();
|