diff options
author | Sebastian <sebasjm@gmail.com> | 2023-10-26 15:26:58 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-10-26 15:26:58 -0300 |
commit | c19aa60d3687260d01404f94e0902fe1943f16df (patch) | |
tree | 027723116667ad0a9f22a92ff472f41b0947da7c /packages/taler-wallet-webextension | |
parent | a4c7bc4b284fe7dc4c65ceaad96fc67c40c9a708 (diff) | |
download | wallet-core-c19aa60d3687260d01404f94e0902fe1943f16df.tar.xz |
first take on adding auto open again, WIP
Diffstat (limited to 'packages/taler-wallet-webextension')
10 files changed, 328 insertions, 97 deletions
diff --git a/packages/taler-wallet-webextension/manifest-v3.json b/packages/taler-wallet-webextension/manifest-v3.json index 93a1a7fc8..bdee05539 100644 --- a/packages/taler-wallet-webextension/manifest-v3.json +++ b/packages/taler-wallet-webextension/manifest-v3.json @@ -15,6 +15,7 @@ "permissions": [ "unlimitedStorage", "storage", + "webRequest", "activeTab", "scripting", "declarativeContent", diff --git a/packages/taler-wallet-webextension/src/hooks/useSettings.ts b/packages/taler-wallet-webextension/src/hooks/useSettings.ts index 7332c15bb..563f3628a 100644 --- a/packages/taler-wallet-webextension/src/hooks/useSettings.ts +++ b/packages/taler-wallet-webextension/src/hooks/useSettings.ts @@ -35,6 +35,7 @@ export const codecForSettings = (): Codec<Settings> => buildCodecForObject<Settings>() .property("walletAllowHttp", codecForBoolean()) .property("injectTalerSupport", codecForBoolean()) + .property("autoOpenByHeader", codecForBoolean()) .property("advanceMode", codecForBoolean()) .property("backup", codecForBoolean()) .property("langSelector", codecForBoolean()) diff --git a/packages/taler-wallet-webextension/src/platform/api.ts b/packages/taler-wallet-webextension/src/platform/api.ts index 44b5959a8..56d668a97 100644 --- a/packages/taler-wallet-webextension/src/platform/api.ts +++ b/packages/taler-wallet-webextension/src/platform/api.ts @@ -16,20 +16,18 @@ import { CoreApiResponse, - NotificationType, TalerUri, - WalletNotification, + WalletNotification } from "@gnu-taler/taler-util"; import { WalletConfig, - WalletConfigParameter, - WalletOperations, + WalletOperations } from "@gnu-taler/taler-wallet-core"; -import { BackgroundOperations } from "../wxApi.js"; import { ExtensionOperations, MessageFromExtension, } from "../taler-wallet-interaction-loader.js"; +import { BackgroundOperations } from "../wxApi.js"; export interface Permissions { /** @@ -48,9 +46,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>; @@ -102,6 +100,7 @@ type WebexWalletConfig = { export interface Settings extends WebexWalletConfig { injectTalerSupport: boolean; + autoOpenByHeader: boolean; advanceMode: boolean; backup: boolean; langSelector: boolean; @@ -112,6 +111,7 @@ export interface Settings extends WebexWalletConfig { export const defaultSettings: Settings = { injectTalerSupport: true, + autoOpenByHeader: true, advanceMode: false, backup: false, langSelector: false, @@ -206,6 +206,15 @@ export interface BackgroundPlatformAPI { message: MessageFromFrontend<Op> & { id: string }, ) => Promise<MessageResponse>, ): void; + + /** + * Backend API + */ + registerTalerHeaderListener( + onHeader: (tabId: number, url: string) => void, + ): void; + + containsTalerHeaderListener(): boolean; } export interface ForegroundPlatformAPI { /** diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts index b0934f107..3151bd6ab 100644 --- a/packages/taler-wallet-webextension/src/platform/chrome.ts +++ b/packages/taler-wallet-webextension/src/platform/chrome.ts @@ -59,6 +59,8 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = { useServiceWorkerAsBackgroundProcess, keepAlive, listenNetworkConnectionState, + registerTalerHeaderListener, + containsTalerHeaderListener, }; export default api; @@ -95,10 +97,6 @@ function isFirefox(): boolean { return false; } -// const hostPermissions = { -// permissions: ["webRequest"], -// origins: ["http://*/*", "https://*/*"], -// }; export function containsClipboardPermissions(): Promise<boolean> { return new Promise((res, rej) => { @@ -113,18 +111,6 @@ 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 async function requestClipboardPermissions(): Promise<boolean> { return new Promise((res, rej) => { res(false); @@ -138,67 +124,7 @@ 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); -// }); -// }); -// } - -// 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; - -// 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; - -// //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); -// }); -// }); -// } + export function removeClipboardPermissions(): Promise<boolean> { return new Promise((res, rej) => { @@ -225,9 +151,9 @@ function addPermissionsListener( function getPermissionsApi(): CrossBrowserPermissionsApi { return { addPermissionsListener, - // containsHostPermissions, - // requestHostPermissions, - // removeHostPermissions, + containsHostPermissions, + requestHostPermissions, + removeHostPermissions, requestClipboardPermissions, removeClipboardPermissions, containsClipboardPermissions, @@ -365,7 +291,7 @@ async function sendMessageToBackground< const timerId = setTimeout(() => { timedout = true; reject(TalerError.fromDetail(TalerErrorCode.GENERIC_TIMEOUT, {}) ); - }, 20 * 1000); //five seconds + }, 20 * 1000); chrome.runtime.sendMessage(messageWithId, (backgroundResponse) => { if (timedout) { return false; //already rejected @@ -782,3 +708,183 @@ function listenNetworkConnectionState( window.removeEventListener("online", notifyOnline); }; } + +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; + + +function containsTalerHeaderListener(): boolean { + return ( + currentHeaderListener !== undefined || currentTabListener !== undefined + ); +} +function registerTalerHeaderListener( + callback: (tabId: number, url: string) => void, +): void { + logger.info("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 hostPermissions = { + permissions: ["webRequest"], + origins: ["http://*/*", "https://*/*"], +}; + +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 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 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; + + //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); + }); + }); +}
\ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/platform/dev.ts b/packages/taler-wallet-webextension/src/platform/dev.ts index 976ac05f5..218422ded 100644 --- a/packages/taler-wallet-webextension/src/platform/dev.ts +++ b/packages/taler-wallet-webextension/src/platform/dev.ts @@ -44,6 +44,8 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = { removeClipboardPermissions: async () => false, requestClipboardPermissions: async () => false, }), + registerTalerHeaderListener: () => false, + containsTalerHeaderListener: () => false, getWalletWebExVersion: () => ({ version: "none", }), diff --git a/packages/taler-wallet-webextension/src/platform/firefox.ts b/packages/taler-wallet-webextension/src/platform/firefox.ts index 9f666e7ae..cc734ebf7 100644 --- a/packages/taler-wallet-webextension/src/platform/firefox.ts +++ b/packages/taler-wallet-webextension/src/platform/firefox.ts @@ -26,6 +26,9 @@ import chromePlatform, { containsClipboardPermissions as chromeClipContains, removeClipboardPermissions as chromeClipRemove, requestClipboardPermissions as chromeClipRequest, + containsHostPermissions as chromeHostContains, + requestHostPermissions as chromeHostRequest, + removeHostPermissions as chromeHostRemove, } from "./chrome.js"; const api: BackgroundPlatformAPI & ForegroundPlatformAPI = { @@ -51,9 +54,9 @@ 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, diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx index da4a20437..c18c0d9bb 100644 --- a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx @@ -43,12 +43,14 @@ const version = { version: "0.9.0.13", hash: "d439c3e1bc743f2aa47de4457953dba6ecb0e20f", }, + }; export const AllOff = tests.createExample(TestedComponent, { deviceName: "this-is-the-device-name", advanceToggle: { value: false, button: {} }, autoOpenToggle: { value: false, button: {} }, + injectTalerToggle: { value: false, button: {} }, langToggle: { value: false, button: {} }, setDeviceName: () => Promise.resolve(), ...version, @@ -58,6 +60,7 @@ export const OneChecked = tests.createExample(TestedComponent, { deviceName: "this-is-the-device-name", advanceToggle: { value: false, button: {} }, autoOpenToggle: { value: false, button: {} }, + injectTalerToggle: { value: false, button: {} }, langToggle: { value: false, button: {} }, setDeviceName: () => Promise.resolve(), ...version, @@ -67,6 +70,7 @@ export const WithOneExchange = tests.createExample(TestedComponent, { deviceName: "this-is-the-device-name", advanceToggle: { value: false, button: {} }, autoOpenToggle: { value: false, button: {} }, + injectTalerToggle: { value: false, button: {} }, langToggle: { value: false, button: {} }, setDeviceName: () => Promise.resolve(), knownExchanges: [ @@ -91,6 +95,7 @@ export const WithExchangeInDifferentState = tests.createExample( deviceName: "this-is-the-device-name", advanceToggle: { value: false, button: {} }, autoOpenToggle: { value: false, button: {} }, + injectTalerToggle: { value: false, button: {} }, langToggle: { value: false, button: {} }, setDeviceName: () => Promise.resolve(), knownExchanges: [ diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx index 45a6db5df..2319fe30a 100644 --- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx @@ -73,6 +73,14 @@ export function SettingsPage(): VNode { deviceName={name} setDeviceName={update} autoOpenToggle={{ + value: settings.autoOpenByHeader, + button: { + onClick: safely("update support injection", async () => { + updateSettings("autoOpenByHeader", !settings.autoOpenByHeader); + }), + }, + }} + injectTalerToggle={{ value: settings.injectTalerSupport, button: { onClick: safely("update support injection", async () => { @@ -109,6 +117,7 @@ export interface ViewProps { deviceName: string; setDeviceName: (s: string) => Promise<void>; autoOpenToggle: ToggleHandler; + injectTalerToggle: ToggleHandler; advanceToggle: ToggleHandler; langToggle: ToggleHandler; knownExchanges: Array<ExchangeListItem>; @@ -122,6 +131,7 @@ export interface ViewProps { export function SettingsView({ knownExchanges, autoOpenToggle, + injectTalerToggle, advanceToggle, langToggle, coreVersion, @@ -207,7 +217,23 @@ export function SettingsView({ <i18n.Translate>Add an exchange</i18n.Translate> </LinkPrimary> </div> - + <EnabledBySettings name="advanceMode"> + <SubTitle> + <i18n.Translate>Navigator</i18n.Translate> + </SubTitle> + <Checkbox + label={i18n.str`Automatically open wallet based on page content`} + name="autoOpen" + description={ + <i18n.Translate> + Enabling this option below will make using the wallet faster, + but requires more permissions from your browser. + </i18n.Translate> + } + enabled={autoOpenToggle.value!} + onToggle={autoOpenToggle.button.onClick!} + /> + </EnabledBySettings> {coreVersion && (<Fragment> {LibtoolVersion.compare(coreVersion.version, WALLET_CORE_SUPPORTED_VERSION)?.compatible ? undefined : @@ -276,8 +302,8 @@ export function SettingsView({ open the wallet using the keyboard shortcut </i18n.Translate> } - enabled={autoOpenToggle.value!} - onToggle={autoOpenToggle.button.onClick!} + enabled={injectTalerToggle.value!} + onToggle={injectTalerToggle.button.onClick!} /> <SubTitle> <i18n.Translate>Version</i18n.Translate> diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts index 21162ccbf..8fb8211ae 100644 --- a/packages/taler-wallet-webextension/src/wxApi.ts +++ b/packages/taler-wallet-webextension/src/wxApi.ts @@ -53,7 +53,7 @@ import { platform } from "./platform/foreground.js"; const logger = new Logger("wxApi"); -export const WALLET_CORE_SUPPORTED_VERSION = "0:0:0" +export const WALLET_CORE_SUPPORTED_VERSION = "1:0:0" export interface ExtendedPermissionsResponse { newValue: boolean; @@ -83,6 +83,14 @@ export interface BackgroundOperations { }; response: void; }; + containsHeaderListener: { + request: void; + response: ExtendedPermissionsResponse; + }; + toggleHeaderListener: { + request: boolean; + response: ExtendedPermissionsResponse; + }; } export interface BackgroundApiClient { diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index 40b7077af..23d3d64fa 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -31,6 +31,7 @@ import { WalletNotification, getErrorDetailFromException, makeErrorDetail, + parseTalerUri, setGlobalLogLevelFromString, setLogLevelFromString, } from "@gnu-taler/taler-util"; @@ -54,7 +55,7 @@ import { import { MessageFromFrontend, MessageResponse } from "./platform/api.js"; import { platform } from "./platform/background.js"; import { ExtensionOperations } from "./taler-wallet-interaction-loader.js"; -import { BackgroundOperations } from "./wxApi.js"; +import { BackgroundOperations, ExtendedPermissionsResponse } from "./wxApi.js"; import { HttpRequestLibrary } from "@gnu-taler/taler-util/http"; /** @@ -132,14 +133,26 @@ async function isInjectionEnabled(): Promise<boolean> { return settings.injectTalerSupport === true; } +async function isHeaderListenerEnabled(): Promise<boolean> { + const settings = await platform.getSettingsFromStorage(); + return settings.autoOpenByHeader === true; +} + const backendHandlers: BackendHandlerType = { freeze, sum, resetDb, runGarbageCollector, setLoggingLevel, + containsHeaderListener, + toggleHeaderListener, }; +async function containsHeaderListener(): Promise<ExtendedPermissionsResponse> { + const result = await platform.containsTalerHeaderListener(); + return { newValue: result }; +} + async function setLoggingLevel({ tag, level, @@ -340,4 +353,61 @@ export async function wxMain(): Promise<void> { } catch (e) { console.error(e); } + // On platforms that support it, also listen to external + // modification of permissions. + platform.getPermissionsApi().addPermissionsListener((perm, lastError) => { + logger.info(`permission added: ${perm}`, ) + if (lastError) { + logger.error( + `there was a problem trying to get permission ${perm}`, + lastError, + ); + return; + } + platform.registerTalerHeaderListener(parseTalerUriAndRedirect); + }); + + if (await isHeaderListenerEnabled()) { + if (await platform.getPermissionsApi().containsHostPermissions()) { + try { + platform.registerTalerHeaderListener(parseTalerUriAndRedirect); + } catch (e) { + logger.error("could not register header listener", e); + } + } else { + await platform.getPermissionsApi().requestHostPermissions() + } + } + +} + + +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 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)}`, + ); +}
\ No newline at end of file |