From 105041c6dabf596a5f3243c0c732326ee9102eaf Mon Sep 17 00:00:00 2001 From: Stephen Altamirano Date: Sat, 24 Feb 2024 21:04:08 -0800 Subject: [PATCH] Fix captcha handling Captcha resolution wasn't IPC compatible. It depended on the renderer process having direct access to `window` in a way that hasn't been true since we upgraded to Electron 12. The current event flow for captcha resolution is a bit roundabout, in that it moves from: contained webview -> main process -> renderer process This could conceivably be shortened, but I don't know that its necessarily valuable to pursue --- src/common/actions/index.ts | 4 ++ src/common/ipc.ts | 17 ++++++- src/main/inject/inject-captcha.ts | 14 +++-- src/main/inject/inject-preload.ts | 16 +----- src/main/main.ts | 6 +++ src/renderer/modal-widgets/RecaptchaInput.tsx | 51 +++++++------------ 6 files changed, 50 insertions(+), 58 deletions(-) diff --git a/src/common/actions/index.ts b/src/common/actions/index.ts index e0df85e54..7d08d1a3f 100644 --- a/src/common/actions/index.ts +++ b/src/common/actions/index.ts @@ -159,6 +159,10 @@ export const actions = wireActions({ initialURL: string; role: WindRole; }>(), + // only exists due to IPC difficulties + closeCaptchaModal: action<{ + response: string; + }>(), // setup diff --git a/src/common/ipc.ts b/src/common/ipc.ts index e98f2b92c..3ee11c5bd 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc.ts @@ -1,4 +1,4 @@ -import { IpcRenderer, OpenDialogOptions } from "electron"; +import { ipcRenderer, IpcRenderer, OpenDialogOptions } from "electron"; export type AsyncIpcHandlers = { showOpenDialog: (o: OpenDialogOptions) => Promise; @@ -11,6 +11,21 @@ export type SyncIpcHandlers = { userAgent: (x: undefined) => string; getImageURL: (p: string) => string; getInjectURL: (p: string) => string; + onCaptchaResponse: (r: string) => null; legacyMarketPath: () => string; mainLogPath: () => string; }; + +export const emitSyncIpcEvent = ( + eventName: K, + arg: Parameters[0] +): ReturnType => { + return ipcRenderer.sendSync(eventName, arg); +}; + +export const emitAsyncIpcEvent = ( + eventName: K, + arg: Parameters[0] +): ReturnType => { + return ipcRenderer.invoke(eventName, arg) as ReturnType; +}; diff --git a/src/main/inject/inject-captcha.ts b/src/main/inject/inject-captcha.ts index 2a6881f94..b09afae27 100644 --- a/src/main/inject/inject-captcha.ts +++ b/src/main/inject/inject-captcha.ts @@ -1,9 +1,7 @@ -interface ExtWindow { - onCaptcha: (response: string) => void; - captchaResponse: string; -} -const extWindow: ExtWindow = window as any; +import { contextBridge } from "electron"; +import { emitSyncIpcEvent } from "common/ipc"; +import "@goosewobbler/electron-redux/preload"; -extWindow.onCaptcha = function (response: string) { - extWindow.captchaResponse = response; -}; +contextBridge.exposeInMainWorld("onCaptcha", function (response: string) { + emitSyncIpcEvent("onCaptchaResponse", response); +}); diff --git a/src/main/inject/inject-preload.ts b/src/main/inject/inject-preload.ts index f4ba08643..5ce2f5cac 100644 --- a/src/main/inject/inject-preload.ts +++ b/src/main/inject/inject-preload.ts @@ -12,7 +12,7 @@ import qs from "querystring"; import { promises } from "fs"; import { Logger } from "common/logger"; import { Message } from "common/helpers/bridge"; -import { AsyncIpcHandlers, SyncIpcHandlers } from "common/ipc"; +import { emitAsyncIpcEvent, emitSyncIpcEvent } from "common/ipc"; import { Store } from "common/types"; import { convertMessage } from "common/helpers/bridge"; import "@goosewobbler/electron-redux/preload"; @@ -27,20 +27,6 @@ const memo = (fn: () => A): (() => A) => { }; }; -const emitSyncIpcEvent = ( - eventName: K, - arg: Parameters[0] -): ReturnType => { - return ipcRenderer.sendSync(eventName, arg); -}; - -const emitAsyncIpcEvent = ( - eventName: K, - arg: Parameters[0] -): ReturnType => { - return ipcRenderer.invoke(eventName, arg) as ReturnType; -}; - export const mainWorldSupplement = { nodeUrl: { parse, format }, electron: { diff --git a/src/main/main.ts b/src/main/main.ts index 36a30fa0c..d65966ae1 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -115,6 +115,12 @@ export function main() { getInjectURL, legacyMarketPath, mainLogPath, + onCaptchaResponse: (response) => { + if (response) { + store.dispatch(actions.closeCaptchaModal({ response })); + } + return null; + }, }, { showOpenDialog: async (options: OpenDialogOptions) => { diff --git a/src/renderer/modal-widgets/RecaptchaInput.tsx b/src/renderer/modal-widgets/RecaptchaInput.tsx index 4a3cdad5b..23a479223 100644 --- a/src/renderer/modal-widgets/RecaptchaInput.tsx +++ b/src/renderer/modal-widgets/RecaptchaInput.tsx @@ -9,6 +9,7 @@ import { hook } from "renderer/hocs/hook"; import styled from "renderer/styles"; import { ModalWidgetProps } from "common/modals"; import modals from "renderer/modals"; +import watching, { Watcher } from "renderer/hocs/watching"; import { RecaptchaInputParams, RecaptchaInputResponse, @@ -35,9 +36,9 @@ const WidgetDiv = styled.div` } `; +@watching class RecaptchaInput extends React.PureComponent { webview: Electron.WebviewTag; - checker: number; constructor(props: RecaptchaInput["props"], context: any) { super(props, context); @@ -46,6 +47,21 @@ class RecaptchaInput extends React.PureComponent { }; } + subscribe(watcher: Watcher) { + watcher.on(actions.closeCaptchaModal, async (store, action) => { + const { dispatch } = this.props; + const { response } = action.payload; + dispatch( + actions.closeModal({ + wind: ambientWind(), + action: modals.recaptchaInput.action({ + recaptchaResponse: response, + }), + }) + ); + }); + } + render() { const params = this.props.modal.widgetParams; const { url } = params; @@ -69,7 +85,6 @@ class RecaptchaInput extends React.PureComponent { gotWebview = (wv: Electron.WebviewTag) => { this.webview = wv; - this.clearChecker(); if (!this.webview) { return; @@ -78,39 +93,7 @@ class RecaptchaInput extends React.PureComponent { this.webview.addEventListener("did-finish-load", () => { this.setState({ loaded: true }); }); - - this.checker = window.setInterval(() => { - this.webview - .executeJavaScript(`window.captchaResponse`, false) - .then((response: string | undefined) => { - if (response) { - const { dispatch } = this.props; - dispatch( - actions.closeModal({ - wind: ambientWind(), - action: modals.recaptchaInput.action({ - recaptchaResponse: response, - }), - }) - ); - } - }) - .catch((e) => { - console.error(e); - }); - }, 500); }; - - componentWillUnmount() { - this.clearChecker(); - } - - clearChecker() { - if (this.checker) { - clearInterval(this.checker); - this.checker = null; - } - } } interface State {