/*
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
*/
/**
* Messaging for the WebExtensions wallet. Should contain
* parts that are specific for WebExtensions, but as little business
* logic as possible.
*/
/**
* Imports.
*/
import {
classifyTalerUri,
CoreApiResponse,
CoreApiResponseSuccess,
Logger,
TalerErrorCode,
TalerUriType,
WalletDiagnostics,
} from "@gnu-taler/taler-util";
import {
DbAccess,
deleteTalerDatabase,
exportDb,
importDb,
makeErrorDetail,
OpenedPromise,
openPromise,
openTalerDatabase,
Wallet,
WalletStoresV1,
} from "@gnu-taler/taler-wallet-core";
import { SetTimeoutTimerAPI } from "@gnu-taler/taler-wallet-core";
import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory.js";
import { BrowserHttpLib } from "./browserHttpLib.js";
import { MessageFromBackend, platform } from "./platform/api.js";
import { SynchronousCryptoWorkerFactory } from "./serviceWorkerCryptoWorkerFactory.js";
import { ServiceWorkerHttpLib } from "./serviceWorkerHttpLib.js";
/**
* Currently active wallet instance. Might be unloaded and
* re-instantiated when the database is reset.
*
* FIXME: Maybe move the wallet resetting into the Wallet class?
*/
let currentWallet: Wallet | undefined;
let currentDatabase: DbAccess | undefined;
/**
* Last version of an outdated DB, if applicable.
*/
let outdatedDbVersion: number | undefined;
const walletInit: OpenedPromise = openPromise();
const logger = new Logger("wxBackend.ts");
async function getDiagnostics(): Promise {
const manifestData = platform.getWalletWebExVersion();
const errors: string[] = [];
let firefoxIdbProblem = false;
let dbOutdated = false;
try {
await walletInit.promise;
} catch (e) {
errors.push("Error during wallet initialization: " + e);
if (
currentDatabase === undefined &&
outdatedDbVersion === undefined &&
platform.isFirefox()
) {
firefoxIdbProblem = true;
}
}
if (!currentWallet) {
errors.push("Could not create wallet backend.");
}
if (!currentDatabase) {
errors.push("Could not open database");
}
if (outdatedDbVersion !== undefined) {
errors.push(`Outdated DB version: ${outdatedDbVersion}`);
dbOutdated = true;
}
const diagnostics: WalletDiagnostics = {
walletManifestDisplayVersion: manifestData.version_name || "(undefined)",
walletManifestVersion: manifestData.version,
errors,
firefoxIdbProblem,
dbOutdated,
};
return diagnostics;
}
async function dispatch(
req: any,
sender: any,
sendResponse: any,
): Promise {
let r: CoreApiResponse;
const wrapResponse = (result: unknown): CoreApiResponseSuccess => {
return {
type: "response",
id: req.id,
operation: req.operation,
result,
};
};
try {
switch (req.operation) {
case "wxGetDiagnostics": {
r = wrapResponse(await getDiagnostics());
break;
}
case "reset-db": {
await deleteTalerDatabase(indexedDB as any);
r = wrapResponse(await reinitWallet());
break;
}
case "run-gc": {
logger.info("gc");
const dump = await exportDb(currentDatabase!.idbHandle());
await deleteTalerDatabase(indexedDB as any);
logger.info("cleaned");
await reinitWallet();
logger.info("init");
await importDb(currentDatabase!.idbHandle(), dump);
logger.info("imported");
r = wrapResponse({ result: true });
break;
}
case "containsHeaderListener": {
const res = await platform.containsTalerHeaderListener();
r = wrapResponse({ newValue: res });
break;
}
case "toggleHeaderListener": {
const newVal = req.payload.value;
logger.trace("new extended permissions value", newVal);
if (newVal) {
platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
r = wrapResponse({ newValue: true });
} else {
const rem = await platform
.getPermissionsApi()
.removeHostPermissions();
logger.trace("permissions removed:", rem);
r = wrapResponse({ newVal: false });
}
break;
}
default: {
const w = currentWallet;
if (!w) {
r = {
type: "error",
id: req.id,
operation: req.operation,
error: makeErrorDetail(
TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
{},
"wallet core not available",
),
};
break;
}
r = await w.handleCoreApiRequest(req.operation, req.id, req.payload);
console.log("response received from wallet", r);
break;
}
}
sendResponse(r);
} catch (e) {
logger.error(`Error sending operation: ${req.operation}`, e);
// might fail if tab disconnected
}
}
async function reinitWallet(): Promise {
if (currentWallet) {
currentWallet.stop();
currentWallet = undefined;
}
currentDatabase = undefined;
// setBadgeText({ text: "" });
try {
currentDatabase = await openTalerDatabase(indexedDB as any, reinitWallet);
} catch (e) {
logger.error("could not open database", e);
walletInit.reject(e);
return;
}
let httpLib;
let cryptoWorker;
let timer;
if (platform.useServiceWorkerAsBackgroundProcess()) {
httpLib = new ServiceWorkerHttpLib();
cryptoWorker = new SynchronousCryptoWorkerFactory();
timer = new SetTimeoutTimerAPI();
} else {
httpLib = new BrowserHttpLib();
// We could (should?) use the BrowserCryptoWorkerFactory here,
// but right now we don't, to have less platform differences.
// cryptoWorker = new BrowserCryptoWorkerFactory();
cryptoWorker = new SynchronousCryptoWorkerFactory();
timer = new SetTimeoutTimerAPI();
}
logger.info("Setting up wallet");
const wallet = await Wallet.create(
currentDatabase,
httpLib,
timer,
cryptoWorker,
);
try {
await wallet.handleCoreApiRequest("initWallet", "native-init", {});
} catch (e) {
logger.error("could not initialize wallet", e);
walletInit.reject(e);
return;
}
wallet.addNotificationListener((x) => {
const message: MessageFromBackend = { type: x.type };
platform.sendMessageToAllChannels(message);
});
platform.keepAlive(() => {
return wallet.runTaskLoop().catch((e) => {
logger.error("error during wallet task loop", e);
});
});
// Useful for debugging in the background page.
if (typeof window !== "undefined") {
(window as any).talerWallet = wallet;
}
currentWallet = wallet;
return walletInit.resolve();
}
function parseTalerUriAndRedirect(tabId: number, talerUri: string): void {
const uriType = classifyTalerUri(talerUri);
switch (uriType) {
case TalerUriType.TalerWithdraw:
return platform.redirectTabToWalletPage(
tabId,
`/cta/withdraw?talerWithdrawUri=${talerUri}`,
);
case TalerUriType.TalerPay:
return platform.redirectTabToWalletPage(
tabId,
`/cta/pay?talerPayUri=${talerUri}`,
);
case TalerUriType.TalerTip:
return platform.redirectTabToWalletPage(
tabId,
`/cta/tip?talerTipUri=${talerUri}`,
);
case TalerUriType.TalerRefund:
return platform.redirectTabToWalletPage(
tabId,
`/cta/refund?talerRefundUri=${talerUri}`,
);
case TalerUriType.TalerPayPull:
return platform.redirectTabToWalletPage(
tabId,
`/cta/invoice/pay?talerPayPullUri=${talerUri}`,
);
case TalerUriType.TalerPayPush:
return platform.redirectTabToWalletPage(
tabId,
`/cta/transfer/pickup?talerPayPushUri=${talerUri}`,
);
case TalerUriType.TalerRecovery:
return platform.redirectTabToWalletPage(
tabId,
`/cta/transfer/recovery?talerBackupUri=${talerUri}`,
);
case TalerUriType.TalerNotifyReserve:
// FIXME: Is this still useful?
// handleNotifyReserve(w);
logger.warn(
`Response with HTTP 402 the Taler header but it is deprecated ${talerUri}`,
);
break;
case TalerUriType.Unknown:
logger.warn(
`Response with HTTP 402 the Taler header but could not classify ${talerUri}`,
);
return;
case TalerUriType.TalerDevExperiment:
// FIXME: Implement!
logger.warn("not implemented");
return;
default: {
const error: never = uriType;
logger.warn(
`Response with HTTP 402 the Taler header "${error}", but header value is not a taler:// URI.`,
);
return;
}
}
}
/**
* Main function to run for the WebExtension backend.
*
* Sets up all event handlers and other machinery.
*/
export async function wxMain(): Promise {
logger.trace("starting");
const afterWalletIsInitialized = reinitWallet();
platform.registerReloadOnNewVersion();
// Handlers for messages coming directly from the content
// script on the page
platform.listenToAllChannels((message, sender, callback) => {
afterWalletIsInitialized.then(() => {
dispatch(message, sender, (response: CoreApiResponse) => {
callback(response);
});
});
});
platform.registerAllIncomingConnections();
try {
platform.registerOnInstalled(() => {
platform.openWalletPage("/welcome");
//
try {
platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
} catch (e) {
logger.error("could not register header listener", e);
}
});
} catch (e) {
console.error(e);
}
// On platforms that support it, also listen to external
// modification of permissions.
platform.getPermissionsApi().addPermissionsListener((perm, lastError) => {
if (lastError) {
logger.error(
`there was a problem trying to get permission ${perm}`,
lastError,
);
return;
}
platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
});
}