check if the script should be injected
remove header listener
This commit is contained in:
parent
b1a0d034fc
commit
b34f3568e8
@ -17,8 +17,7 @@
|
||||
},
|
||||
"permissions": [
|
||||
"unlimitedStorage",
|
||||
"http://*/*",
|
||||
"https://*/*",
|
||||
"storage",
|
||||
"activeTab"
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
@ -28,21 +27,31 @@
|
||||
"dist/taler-wallet-interaction-support.js.map",
|
||||
"dist/taler-wallet-interaction-support.js"
|
||||
],
|
||||
"optional_permissions": [
|
||||
"http://*/*",
|
||||
"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+http",
|
||||
"name": "Taler Wallet WebExtension",
|
||||
"uriTemplate": "/static/wallet.html#/cta/taler-uri/%s"
|
||||
},
|
||||
{
|
||||
"protocol": "web+taler+http",
|
||||
"name": "Taler Wallet WebExtension",
|
||||
"uriTemplate": "/static/wallet.html#/cta/taler-uri/%s"
|
||||
},
|
||||
{
|
||||
"protocol": "ext+taler",
|
||||
"name": "Taler Wallet WebExtension",
|
||||
"uriTemplate": "/static/wallet.html#/cta/withdraw?d=1&talerWithdrawUri=%s"
|
||||
"uriTemplate": "/static/wallet.html#/cta/taler-uri/%s"
|
||||
},
|
||||
{
|
||||
"protocol": "web+taler",
|
||||
"name": "Taler Wallet WebExtension",
|
||||
"uriTemplate": "/static/wallet.html#/cta/taler-uri/%s"
|
||||
}
|
||||
],
|
||||
"browser_action": {
|
||||
|
@ -14,6 +14,7 @@
|
||||
},
|
||||
"permissions": [
|
||||
"unlimitedStorage",
|
||||
"storage",
|
||||
"activeTab",
|
||||
"scripting",
|
||||
"declarativeContent",
|
||||
@ -26,9 +27,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"optional_permissions": [
|
||||
"webRequest"
|
||||
],
|
||||
"content_scripts": [{
|
||||
"id": "taler-wallet-interaction",
|
||||
"matches": ["file://*/*", "http://*/*", "https://*/*"],
|
||||
@ -50,10 +48,6 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"host_permissions": [
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"action": {
|
||||
"default_icon": {
|
||||
"16": "static/img/taler-logo-16.png",
|
||||
|
@ -30,14 +30,8 @@ import { wxMain } from "./wxBackend.js";
|
||||
console.log("Wallet setup for Dev API");
|
||||
setupPlatform(devAPI);
|
||||
|
||||
try {
|
||||
platform.registerOnInstalled(() => {
|
||||
platform.openWalletPage("/welcome");
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
async function start() {
|
||||
await platform.notifyWhenAppIsReady();
|
||||
await wxMain();
|
||||
}
|
||||
|
||||
platform.notifyWhenAppIsReady(() => {
|
||||
wxMain();
|
||||
});
|
||||
start();
|
||||
|
@ -43,6 +43,9 @@ if (isFirefox) {
|
||||
}
|
||||
|
||||
// setGlobalLogLevelFromString("trace")
|
||||
platform.notifyWhenAppIsReady(() => {
|
||||
wxMain();
|
||||
});
|
||||
|
||||
async function start() {
|
||||
await platform.notifyWhenAppIsReady();
|
||||
await wxMain();
|
||||
}
|
||||
start();
|
||||
|
@ -20,6 +20,12 @@ import { useBackendContext } from "../context/backend.js";
|
||||
import { ToggleHandler } from "../mui/handlers.js";
|
||||
import { platform } from "../platform/foreground.js";
|
||||
|
||||
/**
|
||||
* This is not implemented.
|
||||
* Clipboard permission need to get ask the permission to the user
|
||||
* based on user-intention
|
||||
* @returns
|
||||
*/
|
||||
export function useClipboardPermissions(): ToggleHandler {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const api = useBackendContext();
|
||||
@ -40,27 +46,27 @@ export function useClipboardPermissions(): ToggleHandler {
|
||||
}
|
||||
setEnabled(granted);
|
||||
} else {
|
||||
try {
|
||||
await api.background
|
||||
.call("toggleHeaderListener", false)
|
||||
.then((r) => setEnabled(r.newValue));
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
// try {
|
||||
// await api.background
|
||||
// .call("toggleHeaderListener", false)
|
||||
// .then((r) => setEnabled(r.newValue));
|
||||
// } catch (e) {
|
||||
// console.log(e);
|
||||
// }
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function getValue(): Promise<void> {
|
||||
const res = await api.background.call(
|
||||
"containsHeaderListener",
|
||||
undefined,
|
||||
);
|
||||
setEnabled(res.newValue);
|
||||
}
|
||||
getValue();
|
||||
}, []);
|
||||
// useEffect(() => {
|
||||
// async function getValue(): Promise<void> {
|
||||
// const res = await api.background.call(
|
||||
// "containsHeaderListener",
|
||||
// undefined,
|
||||
// );
|
||||
// setEnabled(res.newValue);
|
||||
// }
|
||||
// getValue();
|
||||
// }, []);
|
||||
|
||||
return {
|
||||
value: enabled,
|
||||
|
@ -15,14 +15,7 @@
|
||||
*/
|
||||
|
||||
import { useLocalStorage } from "@gnu-taler/web-util/lib/index.browser";
|
||||
|
||||
interface Settings {
|
||||
injectTalerSupport: boolean;
|
||||
}
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
injectTalerSupport: false,
|
||||
};
|
||||
import { Settings, defaultSettings } from "../platform/api.js";
|
||||
|
||||
function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
|
||||
if (str === undefined) return undefined;
|
||||
@ -42,7 +35,6 @@ export function useSettings(): [
|
||||
const parsed: Settings = parse_json_or_undefined(value) ?? defaultSettings;
|
||||
function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
|
||||
const newValue = { ...parsed, [k]: v };
|
||||
console.log("should update", k, v, parsed, newValue);
|
||||
const json = JSON.stringify(newValue);
|
||||
console.log(json);
|
||||
update(json);
|
||||
|
@ -17,6 +17,10 @@
|
||||
import { CoreApiResponse, NotificationType } from "@gnu-taler/taler-util";
|
||||
import { WalletOperations } from "@gnu-taler/taler-wallet-core";
|
||||
import { BackgroundOperations } from "../wxApi.js";
|
||||
import {
|
||||
ExtensionOperations,
|
||||
MessageFromExtension,
|
||||
} from "../taler-wallet-interaction-loader.js";
|
||||
|
||||
export interface Permissions {
|
||||
/**
|
||||
@ -35,9 +39,9 @@ export interface Permissions {
|
||||
* Compatibility API that works on multiple browsers.
|
||||
*/
|
||||
export interface CrossBrowserPermissionsApi {
|
||||
containsHostPermissions(): Promise<boolean>;
|
||||
requestHostPermissions(): Promise<boolean>;
|
||||
removeHostPermissions(): Promise<boolean>;
|
||||
// containsHostPermissions(): Promise<boolean>;
|
||||
// requestHostPermissions(): Promise<boolean>;
|
||||
// removeHostPermissions(): Promise<boolean>;
|
||||
|
||||
containsClipboardPermissions(): Promise<boolean>;
|
||||
requestClipboardPermissions(): Promise<boolean>;
|
||||
@ -53,9 +57,11 @@ export type MessageFromBackend = {
|
||||
};
|
||||
|
||||
export type MessageFromFrontend<
|
||||
Op extends BackgroundOperations | WalletOperations,
|
||||
Op extends BackgroundOperations | WalletOperations | ExtensionOperations,
|
||||
> = Op extends BackgroundOperations
|
||||
? MessageFromFrontendBackground<keyof BackgroundOperations>
|
||||
: Op extends ExtensionOperations
|
||||
? MessageFromExtension<keyof ExtensionOperations>
|
||||
: Op extends WalletOperations
|
||||
? MessageFromFrontendWallet<keyof WalletOperations>
|
||||
: never;
|
||||
@ -81,11 +87,23 @@ export interface WalletWebExVersion {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
injectTalerSupport: boolean;
|
||||
}
|
||||
|
||||
export const defaultSettings: Settings = {
|
||||
injectTalerSupport: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Compatibility helpers needed for browsers that don't implement
|
||||
* WebExtension APIs consistently.
|
||||
*/
|
||||
export interface BackgroundPlatformAPI {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
getSettingsFromStorage(): Promise<Settings>;
|
||||
/**
|
||||
* Guarantee that the service workers don't die
|
||||
*/
|
||||
@ -116,16 +134,12 @@ export interface BackgroundPlatformAPI {
|
||||
* Register a callback to be called when the wallet is ready to start
|
||||
* @param callback
|
||||
*/
|
||||
notifyWhenAppIsReady(callback: () => void): void;
|
||||
notifyWhenAppIsReady(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the wallet version from manifest
|
||||
*/
|
||||
getWalletWebExVersion(): WalletWebExVersion;
|
||||
/**
|
||||
* Frontend API
|
||||
*/
|
||||
containsTalerHeaderListener(): boolean;
|
||||
/**
|
||||
* Backend API
|
||||
*/
|
||||
@ -134,12 +148,6 @@ export interface BackgroundPlatformAPI {
|
||||
* Backend API
|
||||
*/
|
||||
registerReloadOnNewVersion(): void;
|
||||
/**
|
||||
* Backend API
|
||||
*/
|
||||
registerTalerHeaderListener(
|
||||
onHeader: (tabId: number, url: string) => void,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Permission API for checking and add a listener
|
||||
|
@ -31,10 +31,13 @@ import {
|
||||
MessageFromFrontend,
|
||||
MessageResponse,
|
||||
Permissions,
|
||||
Settings,
|
||||
defaultSettings,
|
||||
} from "./api.js";
|
||||
|
||||
const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
|
||||
isFirefox,
|
||||
getSettingsFromStorage,
|
||||
findTalerUriInActiveTab,
|
||||
findTalerUriInClipboard,
|
||||
getPermissionsApi,
|
||||
@ -49,11 +52,9 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
|
||||
registerOnInstalled,
|
||||
listenToAllChannels: listenToAllChannels as any,
|
||||
registerReloadOnNewVersion,
|
||||
registerTalerHeaderListener,
|
||||
sendMessageToAllChannels,
|
||||
sendMessageToBackground,
|
||||
useServiceWorkerAsBackgroundProcess,
|
||||
containsTalerHeaderListener,
|
||||
keepAlive,
|
||||
};
|
||||
|
||||
@ -61,6 +62,19 @@ export default api;
|
||||
|
||||
const logger = new Logger("chrome.ts");
|
||||
|
||||
async function getSettingsFromStorage(): Promise<Settings> {
|
||||
const data = await chrome.storage.local.get("wallet-settings");
|
||||
if (!data) return defaultSettings;
|
||||
const settings = data["wallet-settings"];
|
||||
if (!settings) return defaultSettings;
|
||||
try {
|
||||
const parsed = JSON.parse(settings);
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
return defaultSettings;
|
||||
}
|
||||
}
|
||||
|
||||
function keepAlive(callback: any): void {
|
||||
if (extensionIsManifestV3()) {
|
||||
chrome.alarms.create("wallet-worker", { periodInMinutes: 1 });
|
||||
@ -78,10 +92,10 @@ function isFirefox(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hostPermissions = {
|
||||
permissions: ["webRequest"],
|
||||
origins: ["http://*/*", "https://*/*"],
|
||||
};
|
||||
// const hostPermissions = {
|
||||
// permissions: ["webRequest"],
|
||||
// origins: ["http://*/*", "https://*/*"],
|
||||
// };
|
||||
|
||||
export function containsClipboardPermissions(): Promise<boolean> {
|
||||
return new Promise((res, rej) => {
|
||||
@ -96,17 +110,17 @@ export function containsClipboardPermissions(): Promise<boolean> {
|
||||
});
|
||||
}
|
||||
|
||||
export function containsHostPermissions(): Promise<boolean> {
|
||||
return new Promise((res, rej) => {
|
||||
chrome.permissions.contains(hostPermissions, (resp) => {
|
||||
const le = chrome.runtime.lastError?.message;
|
||||
if (le) {
|
||||
rej(le);
|
||||
}
|
||||
res(resp);
|
||||
});
|
||||
});
|
||||
}
|
||||
// export function containsHostPermissions(): Promise<boolean> {
|
||||
// return new Promise((res, rej) => {
|
||||
// chrome.permissions.contains(hostPermissions, (resp) => {
|
||||
// const le = chrome.runtime.lastError?.message;
|
||||
// if (le) {
|
||||
// rej(le);
|
||||
// }
|
||||
// res(resp);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
export async function requestClipboardPermissions(): Promise<boolean> {
|
||||
return new Promise((res, rej) => {
|
||||
@ -121,73 +135,67 @@ export async function requestClipboardPermissions(): Promise<boolean> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function requestHostPermissions(): Promise<boolean> {
|
||||
return new Promise((res, rej) => {
|
||||
chrome.permissions.request(hostPermissions, (resp) => {
|
||||
const le = chrome.runtime.lastError?.message;
|
||||
if (le) {
|
||||
rej(le);
|
||||
}
|
||||
res(resp);
|
||||
});
|
||||
});
|
||||
}
|
||||
// export async function requestHostPermissions(): Promise<boolean> {
|
||||
// return new Promise((res, rej) => {
|
||||
// chrome.permissions.request(hostPermissions, (resp) => {
|
||||
// const le = chrome.runtime.lastError?.message;
|
||||
// if (le) {
|
||||
// rej(le);
|
||||
// }
|
||||
// res(resp);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
type HeaderListenerFunc = (
|
||||
details: chrome.webRequest.WebResponseHeadersDetails,
|
||||
) => void;
|
||||
let currentHeaderListener: HeaderListenerFunc | undefined = undefined;
|
||||
// type HeaderListenerFunc = (
|
||||
// details: chrome.webRequest.WebResponseHeadersDetails,
|
||||
// ) => void;
|
||||
// let currentHeaderListener: HeaderListenerFunc | undefined = undefined;
|
||||
|
||||
type TabListenerFunc = (tabId: number, info: chrome.tabs.TabChangeInfo) => void;
|
||||
let currentTabListener: TabListenerFunc | undefined = undefined;
|
||||
// type TabListenerFunc = (tabId: number, info: chrome.tabs.TabChangeInfo) => void;
|
||||
// let currentTabListener: TabListenerFunc | undefined = undefined;
|
||||
|
||||
export function containsTalerHeaderListener(): boolean {
|
||||
return (
|
||||
currentHeaderListener !== undefined || currentTabListener !== undefined
|
||||
);
|
||||
}
|
||||
// export async function removeHostPermissions(): Promise<boolean> {
|
||||
// //if there is a handler already, remove it
|
||||
// if (
|
||||
// currentHeaderListener &&
|
||||
// chrome?.webRequest?.onHeadersReceived?.hasListener(currentHeaderListener)
|
||||
// ) {
|
||||
// chrome.webRequest.onHeadersReceived.removeListener(currentHeaderListener);
|
||||
// }
|
||||
// if (
|
||||
// currentTabListener &&
|
||||
// chrome?.tabs?.onUpdated?.hasListener(currentTabListener)
|
||||
// ) {
|
||||
// chrome.tabs.onUpdated.removeListener(currentTabListener);
|
||||
// }
|
||||
|
||||
export async function removeHostPermissions(): Promise<boolean> {
|
||||
//if there is a handler already, remove it
|
||||
if (
|
||||
currentHeaderListener &&
|
||||
chrome?.webRequest?.onHeadersReceived?.hasListener(currentHeaderListener)
|
||||
) {
|
||||
chrome.webRequest.onHeadersReceived.removeListener(currentHeaderListener);
|
||||
}
|
||||
if (
|
||||
currentTabListener &&
|
||||
chrome?.tabs?.onUpdated?.hasListener(currentTabListener)
|
||||
) {
|
||||
chrome.tabs.onUpdated.removeListener(currentTabListener);
|
||||
}
|
||||
// currentHeaderListener = undefined;
|
||||
// currentTabListener = undefined;
|
||||
|
||||
currentHeaderListener = undefined;
|
||||
currentTabListener = undefined;
|
||||
// //notify the browser about this change, this operation is expensive
|
||||
// if ("webRequest" in chrome) {
|
||||
// chrome.webRequest.handlerBehaviorChanged(() => {
|
||||
// if (chrome.runtime.lastError) {
|
||||
// logger.error(JSON.stringify(chrome.runtime.lastError));
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
//notify the browser about this change, this operation is expensive
|
||||
if ("webRequest" in chrome) {
|
||||
chrome.webRequest.handlerBehaviorChanged(() => {
|
||||
if (chrome.runtime.lastError) {
|
||||
logger.error(JSON.stringify(chrome.runtime.lastError));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (extensionIsManifestV3()) {
|
||||
// Trying to remove host permissions with manifest >= v3 throws an error
|
||||
return true;
|
||||
}
|
||||
return new Promise((res, rej) => {
|
||||
chrome.permissions.remove(hostPermissions, (resp) => {
|
||||
const le = chrome.runtime.lastError?.message;
|
||||
if (le) {
|
||||
rej(le);
|
||||
}
|
||||
res(resp);
|
||||
});
|
||||
});
|
||||
}
|
||||
// if (extensionIsManifestV3()) {
|
||||
// // Trying to remove host permissions with manifest >= v3 throws an error
|
||||
// return true;
|
||||
// }
|
||||
// return new Promise((res, rej) => {
|
||||
// chrome.permissions.remove(hostPermissions, (resp) => {
|
||||
// const le = chrome.runtime.lastError?.message;
|
||||
// if (le) {
|
||||
// rej(le);
|
||||
// }
|
||||
// res(resp);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
export function removeClipboardPermissions(): Promise<boolean> {
|
||||
return new Promise((res, rej) => {
|
||||
@ -214,9 +222,9 @@ function addPermissionsListener(
|
||||
function getPermissionsApi(): CrossBrowserPermissionsApi {
|
||||
return {
|
||||
addPermissionsListener,
|
||||
containsHostPermissions,
|
||||
requestHostPermissions,
|
||||
removeHostPermissions,
|
||||
// containsHostPermissions,
|
||||
// requestHostPermissions,
|
||||
// removeHostPermissions,
|
||||
requestClipboardPermissions,
|
||||
removeClipboardPermissions,
|
||||
containsClipboardPermissions,
|
||||
@ -227,12 +235,16 @@ function getPermissionsApi(): CrossBrowserPermissionsApi {
|
||||
*
|
||||
* @param callback function to be called
|
||||
*/
|
||||
function notifyWhenAppIsReady(callback: () => void): void {
|
||||
if (extensionIsManifestV3()) {
|
||||
callback();
|
||||
} else {
|
||||
window.addEventListener("load", callback);
|
||||
}
|
||||
function notifyWhenAppIsReady(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (extensionIsManifestV3()) {
|
||||
resolve();
|
||||
} else {
|
||||
window.addEventListener("load", () => {
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openWalletURIFromPopup(maybeTalerUri: string): void {
|
||||
@ -478,101 +490,6 @@ function getWalletWebExVersion(): WalletVersion {
|
||||
return manifestData;
|
||||
}
|
||||
|
||||
function registerTalerHeaderListener(
|
||||
callback: (tabId: number, url: string) => void,
|
||||
): void {
|
||||
logger.trace("setting up header listener");
|
||||
|
||||
function headerListener(
|
||||
details: chrome.webRequest.WebResponseHeadersDetails,
|
||||
): void {
|
||||
if (chrome.runtime.lastError) {
|
||||
logger.error(JSON.stringify(chrome.runtime.lastError));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
details.statusCode === 402 ||
|
||||
details.statusCode === 202 ||
|
||||
details.statusCode === 200
|
||||
) {
|
||||
const values = (details.responseHeaders || [])
|
||||
.filter((h) => h.name.toLowerCase() === "taler")
|
||||
.map((h) => h.value)
|
||||
.filter((value): value is string => !!value);
|
||||
if (values.length > 0) {
|
||||
logger.info(
|
||||
`Found a Taler URI in a response header for the request ${details.url} from tab ${details.tabId}`,
|
||||
);
|
||||
callback(details.tabId, values[0]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async function tabListener(
|
||||
tabId: number,
|
||||
info: chrome.tabs.TabChangeInfo,
|
||||
): Promise<void> {
|
||||
if (tabId < 0) return;
|
||||
const tabLocationHasBeenUpdated = info.status === "complete";
|
||||
const tabTitleHasBeenUpdated = info.title !== undefined;
|
||||
if (tabLocationHasBeenUpdated || tabTitleHasBeenUpdated) {
|
||||
const uri = await findTalerUriInTab(tabId);
|
||||
if (!uri) return;
|
||||
logger.info(`Found a Taler URI in the tab ${tabId}`);
|
||||
callback(tabId, uri);
|
||||
}
|
||||
}
|
||||
|
||||
const prevHeaderListener = currentHeaderListener;
|
||||
const prevTabListener = currentTabListener;
|
||||
|
||||
getPermissionsApi()
|
||||
.containsHostPermissions()
|
||||
.then((result) => {
|
||||
//if there is a handler already, remove it
|
||||
if (
|
||||
prevHeaderListener &&
|
||||
chrome?.webRequest?.onHeadersReceived?.hasListener(prevHeaderListener)
|
||||
) {
|
||||
chrome.webRequest.onHeadersReceived.removeListener(prevHeaderListener);
|
||||
}
|
||||
if (
|
||||
prevTabListener &&
|
||||
chrome?.tabs?.onUpdated?.hasListener(prevTabListener)
|
||||
) {
|
||||
chrome.tabs.onUpdated.removeListener(prevTabListener);
|
||||
}
|
||||
|
||||
//if the result was positive, add the headerListener
|
||||
if (result) {
|
||||
const headersEvent:
|
||||
| chrome.webRequest.WebResponseHeadersEvent
|
||||
| undefined = chrome?.webRequest?.onHeadersReceived;
|
||||
if (headersEvent) {
|
||||
headersEvent.addListener(headerListener, { urls: ["<all_urls>"] }, [
|
||||
"responseHeaders",
|
||||
]);
|
||||
currentHeaderListener = headerListener;
|
||||
}
|
||||
|
||||
const tabsEvent: chrome.tabs.TabUpdatedEvent | undefined =
|
||||
chrome?.tabs?.onUpdated;
|
||||
if (tabsEvent) {
|
||||
tabsEvent.addListener(tabListener);
|
||||
currentTabListener = tabListener;
|
||||
}
|
||||
}
|
||||
|
||||
//notify the browser about this change, this operation is expensive
|
||||
chrome?.webRequest?.handlerBehaviorChanged(() => {
|
||||
if (chrome.runtime.lastError) {
|
||||
logger.error(JSON.stringify(chrome.runtime.lastError));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const alertIcons = {
|
||||
"16": "/static/img/taler-alert-16.png",
|
||||
"19": "/static/img/taler-alert-19.png",
|
||||
@ -750,7 +667,7 @@ function registerOnInstalled(callback: () => void): void {
|
||||
if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) {
|
||||
callback();
|
||||
}
|
||||
registerIconChangeOnTalerContent();
|
||||
await registerIconChangeOnTalerContent();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -23,18 +23,17 @@ import {
|
||||
MessageFromBackend,
|
||||
MessageFromFrontend,
|
||||
MessageResponse,
|
||||
defaultSettings,
|
||||
} from "./api.js";
|
||||
|
||||
const frames = ["popup", "wallet"];
|
||||
|
||||
const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
|
||||
isFirefox: () => false,
|
||||
getSettingsFromStorage: () => Promise.resolve(defaultSettings),
|
||||
keepAlive: (cb: VoidFunction) => cb(),
|
||||
findTalerUriInActiveTab: async () => undefined,
|
||||
findTalerUriInClipboard: async () => undefined,
|
||||
containsTalerHeaderListener: () => {
|
||||
return true;
|
||||
},
|
||||
getPermissionsApi: () => ({
|
||||
addPermissionsListener: () => undefined,
|
||||
containsHostPermissions: async () => true,
|
||||
@ -47,21 +46,23 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
|
||||
getWalletWebExVersion: () => ({
|
||||
version: "none",
|
||||
}),
|
||||
notifyWhenAppIsReady: (fn: () => void) => {
|
||||
notifyWhenAppIsReady: () => {
|
||||
let total = frames.length;
|
||||
function waitAndNotify(): void {
|
||||
total--;
|
||||
if (total < 1) {
|
||||
fn();
|
||||
}
|
||||
}
|
||||
frames.forEach((f) => {
|
||||
const theFrame = window.frames[f as any];
|
||||
if (theFrame.location.href === "about:blank") {
|
||||
waitAndNotify();
|
||||
} else {
|
||||
theFrame.addEventListener("load", waitAndNotify);
|
||||
return new Promise((fn) => {
|
||||
function waitAndNotify(): void {
|
||||
total--;
|
||||
if (total < 1) {
|
||||
fn();
|
||||
}
|
||||
}
|
||||
frames.forEach((f) => {
|
||||
const theFrame = window.frames[f as any];
|
||||
if (theFrame.location.href === "about:blank") {
|
||||
waitAndNotify();
|
||||
} else {
|
||||
theFrame.addEventListener("load", waitAndNotify);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@ -80,9 +81,8 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
|
||||
},
|
||||
|
||||
registerAllIncomingConnections: () => undefined,
|
||||
registerOnInstalled: (fn: () => void) => undefined,
|
||||
registerOnInstalled: () => Promise.resolve(),
|
||||
registerReloadOnNewVersion: () => undefined,
|
||||
registerTalerHeaderListener: () => undefined,
|
||||
|
||||
useServiceWorkerAsBackgroundProcess: () => false,
|
||||
|
||||
|
@ -19,11 +19,10 @@ import {
|
||||
CrossBrowserPermissionsApi,
|
||||
ForegroundPlatformAPI,
|
||||
Permissions,
|
||||
Settings,
|
||||
defaultSettings,
|
||||
} from "./api.js";
|
||||
import chromePlatform, {
|
||||
containsHostPermissions as chromeHostContains,
|
||||
removeHostPermissions as chromeHostRemove,
|
||||
requestHostPermissions as chromeHostRequest,
|
||||
containsClipboardPermissions as chromeClipContains,
|
||||
removeClipboardPermissions as chromeClipRemove,
|
||||
requestClipboardPermissions as chromeClipRequest,
|
||||
@ -32,6 +31,7 @@ import chromePlatform, {
|
||||
const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
|
||||
...chromePlatform,
|
||||
isFirefox,
|
||||
getSettingsFromStorage,
|
||||
getPermissionsApi,
|
||||
notifyWhenAppIsReady,
|
||||
redirectTabToWalletPage,
|
||||
@ -51,25 +51,43 @@ function addPermissionsListener(callback: (p: Permissions) => void): void {
|
||||
function getPermissionsApi(): CrossBrowserPermissionsApi {
|
||||
return {
|
||||
addPermissionsListener,
|
||||
containsHostPermissions: chromeHostContains,
|
||||
requestHostPermissions: chromeHostRequest,
|
||||
removeHostPermissions: chromeHostRemove,
|
||||
// containsHostPermissions: chromeHostContains,
|
||||
// requestHostPermissions: chromeHostRequest,
|
||||
// removeHostPermissions: chromeHostRemove,
|
||||
containsClipboardPermissions: chromeClipContains,
|
||||
removeClipboardPermissions: chromeClipRemove,
|
||||
requestClipboardPermissions: chromeClipRequest,
|
||||
};
|
||||
}
|
||||
|
||||
async function getSettingsFromStorage(): Promise<Settings> {
|
||||
//@ts-ignore
|
||||
const data = await browser.storage.local.get("wallet-settings");
|
||||
if (!data) return defaultSettings;
|
||||
const settings = data["wallet-settings"];
|
||||
if (!settings) return defaultSettings;
|
||||
try {
|
||||
const parsed = JSON.parse(settings);
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
return defaultSettings;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param callback function to be called
|
||||
*/
|
||||
function notifyWhenAppIsReady(callback: () => void): void {
|
||||
if (chrome.runtime && chrome.runtime.getManifest().manifest_version === 3) {
|
||||
callback();
|
||||
} else {
|
||||
window.addEventListener("load", callback);
|
||||
}
|
||||
function notifyWhenAppIsReady(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (chrome.runtime && chrome.runtime.getManifest().manifest_version === 3) {
|
||||
resolve();
|
||||
} else {
|
||||
window.addEventListener("load", () => {
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function redirectTabToWalletPage(tabId: number, page: string): void {
|
||||
|
@ -14,6 +14,8 @@
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import { CoreApiResponse } from "@gnu-taler/taler-util";
|
||||
|
||||
/**
|
||||
* This will modify all the pages that the user load when navigating with Web Extension enabled
|
||||
*
|
||||
@ -62,7 +64,7 @@ const logger = {
|
||||
console.error(`${new Date().toISOString()} TALER`, ...msg),
|
||||
};
|
||||
|
||||
function start() {
|
||||
async function start() {
|
||||
if (shouldNotInject) {
|
||||
return;
|
||||
}
|
||||
@ -73,8 +75,15 @@ function start() {
|
||||
}
|
||||
createBridgeWithExtension();
|
||||
logger.debug("bridged created");
|
||||
injectTalerSupportScript(debugEnabled);
|
||||
logger.debug("done");
|
||||
|
||||
const shouldInject = await callBackground("isInjectionEnabled", undefined);
|
||||
|
||||
if (shouldInject) {
|
||||
injectTalerSupportScript(debugEnabled);
|
||||
logger.debug("injection completed");
|
||||
} else {
|
||||
logger.debug("injection is not enabled");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -132,4 +141,65 @@ function createBridgeWithExtension() {
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
start();
|
||||
|
@ -92,7 +92,7 @@ function buildApi(config: Readonly<Info>): API {
|
||||
}
|
||||
const targetAttr = ev.currentTarget.attributes.getNamedItem("target");
|
||||
const windowTarget =
|
||||
targetAttr && targetAttr.value ? targetAttr.value : "taler-wallet";
|
||||
targetAttr && targetAttr.value ? targetAttr.value : "_self";
|
||||
const page = convertURIToWebExtensionPath(hrefAttr.value);
|
||||
if (!page) {
|
||||
logger.debug(`onclick: could not convert "${hrefAttr.value}" into path`);
|
||||
|
@ -271,7 +271,7 @@ export function SettingsView({
|
||||
<i18n.Translate>Navigator</i18n.Translate>
|
||||
</SubTitle>
|
||||
<Checkbox
|
||||
label={i18n.str`Automatically inject Taler API in all pages`}
|
||||
label={i18n.str`Inject Taler support in all pages`}
|
||||
name="inject"
|
||||
description={
|
||||
<i18n.Translate>
|
||||
|
@ -100,7 +100,7 @@ export function View({
|
||||
<i18n.Translate>Navigator</i18n.Translate>
|
||||
</SubTitle>
|
||||
<Checkbox
|
||||
label={i18n.str`Automatically inject Taler API in all pages`}
|
||||
label={i18n.str`Inject Taler support in all pages`}
|
||||
name="inject"
|
||||
description={
|
||||
<i18n.Translate>
|
||||
|
@ -68,18 +68,10 @@ export interface BackgroundOperations {
|
||||
request: void;
|
||||
response: void;
|
||||
};
|
||||
containsHeaderListener: {
|
||||
request: void;
|
||||
response: ExtendedPermissionsResponse;
|
||||
};
|
||||
getDiagnostics: {
|
||||
request: void;
|
||||
response: WalletDiagnostics;
|
||||
};
|
||||
toggleHeaderListener: {
|
||||
request: boolean;
|
||||
response: ExtendedPermissionsResponse;
|
||||
};
|
||||
runGarbageCollector: {
|
||||
request: void;
|
||||
response: void;
|
||||
|
@ -24,42 +24,39 @@
|
||||
* Imports.
|
||||
*/
|
||||
import {
|
||||
classifyTalerUri,
|
||||
Logger,
|
||||
LogLevel,
|
||||
Logger,
|
||||
TalerErrorCode,
|
||||
WalletDiagnostics,
|
||||
getErrorDetailFromException,
|
||||
makeErrorDetail,
|
||||
setGlobalLogLevelFromString,
|
||||
setLogLevelFromString,
|
||||
TalerErrorCode,
|
||||
TalerUriType,
|
||||
WalletDiagnostics,
|
||||
makeErrorDetail,
|
||||
getErrorDetailFromException,
|
||||
parseTalerUri,
|
||||
TalerUriAction,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import {
|
||||
DbAccess,
|
||||
deleteTalerDatabase,
|
||||
exportDb,
|
||||
importDb,
|
||||
OpenedPromise,
|
||||
openPromise,
|
||||
openTalerDatabase,
|
||||
SetTimeoutTimerAPI,
|
||||
Wallet,
|
||||
WalletOperations,
|
||||
WalletStoresV1,
|
||||
deleteTalerDatabase,
|
||||
exportDb,
|
||||
importDb,
|
||||
openPromise,
|
||||
openTalerDatabase,
|
||||
} from "@gnu-taler/taler-wallet-core";
|
||||
import { BrowserHttpLib } from "./browserHttpLib.js";
|
||||
import { platform } from "./platform/background.js";
|
||||
import {
|
||||
MessageFromBackend,
|
||||
MessageFromFrontend,
|
||||
MessageResponse,
|
||||
} from "./platform/api.js";
|
||||
import { platform } from "./platform/background.js";
|
||||
import { SynchronousCryptoWorkerFactory } from "./serviceWorkerCryptoWorkerFactory.js";
|
||||
import { ServiceWorkerHttpLib } from "./serviceWorkerHttpLib.js";
|
||||
import { BackgroundOperations, ExtendedPermissionsResponse } from "./wxApi.js";
|
||||
import { ExtensionOperations } from "./taler-wallet-interaction-loader.js";
|
||||
import { BackgroundOperations } from "./wxApi.js";
|
||||
|
||||
/**
|
||||
* Currently active wallet instance. Might be unloaded and
|
||||
@ -123,10 +120,11 @@ type BackendHandlerType = {
|
||||
) => Promise<BackgroundOperations[Op]["response"]>;
|
||||
};
|
||||
|
||||
async function containsHeaderListener(): Promise<ExtendedPermissionsResponse> {
|
||||
const result = await platform.containsTalerHeaderListener();
|
||||
return { newValue: result };
|
||||
}
|
||||
type ExtensionHandlerType = {
|
||||
[Op in keyof ExtensionOperations]: (
|
||||
req: ExtensionOperations[Op]["request"],
|
||||
) => Promise<ExtensionOperations[Op]["response"]>;
|
||||
};
|
||||
|
||||
async function resetDb(): Promise<void> {
|
||||
await deleteTalerDatabase(indexedDB as any);
|
||||
@ -153,20 +151,6 @@ async function runGarbageCollector(): Promise<void> {
|
||||
logger.info("imported");
|
||||
}
|
||||
|
||||
async function toggleHeaderListener(
|
||||
newVal: boolean,
|
||||
): Promise<ExtendedPermissionsResponse> {
|
||||
logger.trace("new extended permissions value", newVal);
|
||||
if (newVal) {
|
||||
platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
|
||||
return { newValue: true };
|
||||
}
|
||||
|
||||
const rem = await platform.getPermissionsApi().removeHostPermissions();
|
||||
logger.trace("permissions removed:", rem);
|
||||
return { newValue: false };
|
||||
}
|
||||
|
||||
function freeze(time: number): Promise<void> {
|
||||
return new Promise((res, rej) => {
|
||||
setTimeout(res, time);
|
||||
@ -177,14 +161,21 @@ async function sum(ns: Array<number>): Promise<number> {
|
||||
return ns.reduce((prev, cur) => prev + cur, 0);
|
||||
}
|
||||
|
||||
const extensionHandlers: ExtensionHandlerType = {
|
||||
isInjectionEnabled,
|
||||
};
|
||||
|
||||
async function isInjectionEnabled(): Promise<boolean> {
|
||||
const settings = await platform.getSettingsFromStorage();
|
||||
return settings.injectTalerSupport === true;
|
||||
}
|
||||
|
||||
const backendHandlers: BackendHandlerType = {
|
||||
freeze,
|
||||
sum,
|
||||
containsHeaderListener,
|
||||
getDiagnostics,
|
||||
resetDb,
|
||||
runGarbageCollector,
|
||||
toggleHeaderListener,
|
||||
setLoggingLevel,
|
||||
};
|
||||
|
||||
@ -203,55 +194,85 @@ async function setLoggingLevel({
|
||||
}
|
||||
}
|
||||
|
||||
async function dispatch<Op extends WalletOperations | BackgroundOperations>(
|
||||
req: MessageFromFrontend<Op> & { id: string },
|
||||
): Promise<MessageResponse> {
|
||||
if (req.channel === "background") {
|
||||
const handler = backendHandlers[req.operation] as (req: any) => any;
|
||||
if (!handler) {
|
||||
return {
|
||||
type: "error",
|
||||
id: req.id,
|
||||
operation: String(req.operation),
|
||||
error: getErrorDetailFromException(
|
||||
Error(`unknown background operation`),
|
||||
),
|
||||
};
|
||||
async function dispatch<
|
||||
Op extends WalletOperations | BackgroundOperations | ExtensionOperations,
|
||||
>(req: MessageFromFrontend<Op> & { id: string }): Promise<MessageResponse> {
|
||||
switch (req.channel) {
|
||||
case "background": {
|
||||
const handler = backendHandlers[req.operation] as (req: any) => any;
|
||||
if (!handler) {
|
||||
return {
|
||||
type: "error",
|
||||
id: req.id,
|
||||
operation: String(req.operation),
|
||||
error: getErrorDetailFromException(
|
||||
Error(`unknown background operation`),
|
||||
),
|
||||
};
|
||||
}
|
||||
try {
|
||||
const result = await handler(req.payload);
|
||||
return {
|
||||
type: "response",
|
||||
id: req.id,
|
||||
operation: String(req.operation),
|
||||
result,
|
||||
};
|
||||
} catch (er) {
|
||||
return {
|
||||
type: "error",
|
||||
id: req.id,
|
||||
error: getErrorDetailFromException(er),
|
||||
operation: String(req.operation),
|
||||
};
|
||||
}
|
||||
}
|
||||
try {
|
||||
const result = await handler(req.payload);
|
||||
return {
|
||||
type: "response",
|
||||
id: req.id,
|
||||
operation: String(req.operation),
|
||||
result,
|
||||
};
|
||||
} catch (er) {
|
||||
return {
|
||||
type: "error",
|
||||
id: req.id,
|
||||
error: getErrorDetailFromException(er),
|
||||
operation: String(req.operation),
|
||||
};
|
||||
case "extension": {
|
||||
const handler = extensionHandlers[req.operation] as (req: any) => any;
|
||||
if (!handler) {
|
||||
return {
|
||||
type: "error",
|
||||
id: req.id,
|
||||
operation: String(req.operation),
|
||||
error: getErrorDetailFromException(
|
||||
Error(`unknown extension operation`),
|
||||
),
|
||||
};
|
||||
}
|
||||
try {
|
||||
const result = await handler(req.payload);
|
||||
return {
|
||||
type: "response",
|
||||
id: req.id,
|
||||
operation: String(req.operation),
|
||||
result,
|
||||
};
|
||||
} catch (er) {
|
||||
return {
|
||||
type: "error",
|
||||
id: req.id,
|
||||
error: getErrorDetailFromException(er),
|
||||
operation: String(req.operation),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
case "wallet": {
|
||||
const w = currentWallet;
|
||||
if (!w) {
|
||||
return {
|
||||
type: "error",
|
||||
id: req.id,
|
||||
operation: req.operation,
|
||||
error: makeErrorDetail(
|
||||
TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
|
||||
{},
|
||||
"wallet core not available",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (req.channel === "wallet") {
|
||||
const w = currentWallet;
|
||||
if (!w) {
|
||||
return {
|
||||
type: "error",
|
||||
id: req.id,
|
||||
operation: req.operation,
|
||||
error: makeErrorDetail(
|
||||
TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
|
||||
{},
|
||||
"wallet core not available",
|
||||
),
|
||||
};
|
||||
return await w.handleCoreApiRequest(req.operation, req.id, req.payload);
|
||||
}
|
||||
|
||||
return await w.handleCoreApiRequest(req.operation, req.id, req.payload);
|
||||
}
|
||||
|
||||
const anyReq = req as any;
|
||||
@ -261,7 +282,7 @@ async function dispatch<Op extends WalletOperations | BackgroundOperations>(
|
||||
operation: String(anyReq.operation),
|
||||
error: getErrorDetailFromException(
|
||||
Error(
|
||||
`unknown channel ${anyReq.channel}, should be "background" or "wallet"`,
|
||||
`unknown channel ${anyReq.channel}, should be "background", "extension" or "wallet"`,
|
||||
),
|
||||
),
|
||||
};
|
||||
@ -330,23 +351,6 @@ async function reinitWallet(): Promise<void> {
|
||||
return walletInit.resolve();
|
||||
}
|
||||
|
||||
function parseTalerUriAndRedirect(tabId: number, maybeTalerUri: string): void {
|
||||
const talerUri = maybeTalerUri.startsWith("ext+")
|
||||
? maybeTalerUri.substring(4)
|
||||
: maybeTalerUri;
|
||||
const uri = parseTalerUri(talerUri);
|
||||
if (!uri) {
|
||||
logger.warn(
|
||||
`Response with HTTP 402 the Taler header but could not classify ${talerUri}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
return platform.redirectTabToWalletPage(
|
||||
tabId,
|
||||
`/taler-uri/${encodeURIComponent(talerUri)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to run for the WebExtension backend.
|
||||
*
|
||||
@ -370,30 +374,10 @@ export async function wxMain(): Promise<void> {
|
||||
platform.registerAllIncomingConnections();
|
||||
|
||||
try {
|
||||
platform.registerOnInstalled(() => {
|
||||
await 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);
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user