refactor(cef): simplify popup bridge handling
- commit
- 3101cceb3e7c4c49586e5929c4ae5db4fd460ec4
- author
- brice · · 2026-04-21 14:21 UTC
- parent
- 830fe74
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
12 files · +250 -142
M internal/application/port/webview.go +5 -0
| @@ -5,11 +5,16 @@ package port | |||
| 5 | 5 | ||
| 6 | 6 | import ( | |
| 7 | 7 | "context" | |
| 8 | + | "errors" | |
| 8 | 9 | ) | |
| 9 | 10 | ||
| 10 | 11 | // WebViewID uniquely identifies a WebView instance. | |
| 11 | 12 | type WebViewID uint64 | |
| 12 | 13 | ||
| 14 | + | // ErrRelatedWebViewUnsupported reports that the active engine cannot create | |
| 15 | + | // a popup WebView related to an existing parent WebView. | |
| 16 | + | var ErrRelatedWebViewUnsupported = errors.New("related popup webviews are not supported") | |
| 17 | + | ||
| 13 | 18 | // LoadEvent represents page load state transitions. | |
| 14 | 19 | type LoadEvent int | |
| 15 | 20 | ||
| @@ -5,11 +5,16 @@ package port | |||||
| 5 | 5 | ||||
| 6 | import ( | 6 | import ( | ||
| 7 | "context" | 7 | "context" | ||
| 8 | + | "errors" | |||
| 8 | ) | 9 | ) | ||
| 9 | 10 | ||||
| 10 | // WebViewID uniquely identifies a WebView instance. | 11 | // WebViewID uniquely identifies a WebView instance. | ||
| 11 | type WebViewID uint64 | 12 | type WebViewID uint64 | ||
| 12 | 13 | ||||
| 14 | + | // ErrRelatedWebViewUnsupported reports that the active engine cannot create | |||
| 15 | + | // a popup WebView related to an existing parent WebView. | |||
| 16 | + | var ErrRelatedWebViewUnsupported = errors.New("related popup webviews are not supported") | |||
| 17 | + | ||||
| 13 | // LoadEvent represents page load state transitions. | 18 | // LoadEvent represents page load state transitions. | ||
| 14 | type LoadEvent int | 19 | type LoadEvent int | ||
| 15 | 20 | ||||
M internal/infrastructure/cef/engine.go +34 -24
| @@ -49,8 +49,9 @@ type Engine struct { | |||
| 49 | 49 | alreadyRunningAppRelaunchHandler func(string) | |
| 50 | 50 | ||
| 51 | 51 | // activeWebViews tracks all live webviews for CSS broadcast. | |
| 52 | - | activeWebViews sync.Map // map[port.WebViewID]*WebView | |
| 53 | - | activeCount atomic.Int32 | |
| 52 | + | activeWebViews sync.Map // map[port.WebViewID]*WebView | |
| 53 | + | browserWebViews sync.Map // map[int32]*WebView | |
| 54 | + | activeCount atomic.Int32 | |
| 54 | 55 | ||
| 55 | 56 | // shutdownNotify is signaled when the active webview count reaches 0 | |
| 56 | 57 | // during shutdown, replacing the busy-wait poll in closeActiveWebViews. | |
| @@ -145,10 +146,29 @@ func (e *Engine) registerWebView(wv *WebView) { | |||
| 145 | 146 | e.activeCount.Add(1) | |
| 146 | 147 | } | |
| 147 | 148 | ||
| 149 | + | func (e *Engine) bindBrowserWebView(browser purecef.Browser, wv *WebView) { | |
| 150 | + | if e == nil || browser == nil || wv == nil { | |
| 151 | + | return | |
| 152 | + | } | |
| 153 | + | e.browserWebViews.Store(browser.GetIdentifier(), wv) | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | func (e *Engine) unbindBrowserWebView(browserID int32, wv *WebView) { | |
| 157 | + | if e == nil || browserID == 0 { | |
| 158 | + | return | |
| 159 | + | } | |
| 160 | + | if current, ok := e.browserWebViews.Load(browserID); ok { | |
| 161 | + | if existing, ok := current.(*WebView); ok && existing == wv { | |
| 162 | + | e.browserWebViews.Delete(browserID) | |
| 163 | + | } | |
| 164 | + | } | |
| 165 | + | } | |
| 166 | + | ||
| 148 | 167 | // unregisterWebView removes a webview from the active tracking map. | |
| 149 | 168 | // If the count reaches 0 and a shutdown is in progress, it signals shutdownNotify. | |
| 150 | - | func (e *Engine) unregisterWebView(wv *WebView) { | |
| 169 | + | func (e *Engine) unregisterWebView(wv *WebView, browserID int32) { | |
| 151 | 170 | e.activeWebViews.Delete(wv.id) | |
| 171 | + | e.unbindBrowserWebView(browserID, wv) | |
| 152 | 172 | if e.activeCount.Add(-1) == 0 && e.shutdownNotify != nil { | |
| 153 | 173 | select { | |
| 154 | 174 | case e.shutdownNotify <- struct{}{}: | |
| @@ -432,28 +452,18 @@ func (e *Engine) validateBridgeRequest(browser purecef.Browser, bridgeNonce stri | |||
| 432 | 452 | return false | |
| 433 | 453 | } | |
| 434 | 454 | ||
| 435 | - | browserID := browser.GetIdentifier() | |
| 436 | - | valid := false | |
| 437 | - | e.activeWebViews.Range(func(_, value any) bool { | |
| 438 | - | wv, ok := value.(*WebView) | |
| 439 | - | if !ok || wv == nil { | |
| 440 | - | return true | |
| 441 | - | } | |
| 455 | + | wv := e.webViewForBrowser(browser) | |
| 456 | + | if wv == nil { | |
| 457 | + | return false | |
| 458 | + | } | |
| 442 | 459 | ||
| 443 | - | wv.mu.RLock() | |
| 444 | - | wvBrowser := wv.browser | |
| 445 | - | wvBridgeNonce := wv.bridgeNonce | |
| 446 | - | wv.mu.RUnlock() | |
| 447 | - | if wvBrowser == nil || wvBridgeNonce == "" || wvBrowser.GetIdentifier() != browserID { | |
| 448 | - | return true | |
| 449 | - | } | |
| 450 | - | if subtle.ConstantTimeCompare([]byte(wvBridgeNonce), []byte(bridgeNonce)) == 1 { | |
| 451 | - | valid = true | |
| 452 | - | return false | |
| 453 | - | } | |
| 454 | - | return true | |
| 455 | - | }) | |
| 456 | - | return valid | |
| 460 | + | wv.mu.RLock() | |
| 461 | + | wvBridgeNonce := wv.bridgeNonce | |
| 462 | + | wv.mu.RUnlock() | |
| 463 | + | if wvBridgeNonce == "" { | |
| 464 | + | return false | |
| 465 | + | } | |
| 466 | + | return subtle.ConstantTimeCompare([]byte(wvBridgeNonce), []byte(bridgeNonce)) == 1 | |
| 457 | 467 | } | |
| 458 | 468 | ||
| 459 | 469 | func (e *Engine) currentDownloadHandler() *downloadHandler { | |
| @@ -49,8 +49,9 @@ type Engine struct { | |||||
| 49 | alreadyRunningAppRelaunchHandler func(string) | 49 | alreadyRunningAppRelaunchHandler func(string) | ||
| 50 | 50 | ||||
| 51 | // activeWebViews tracks all live webviews for CSS broadcast. | 51 | // activeWebViews tracks all live webviews for CSS broadcast. | ||
| 52 | - | activeWebViews sync.Map // map[port.WebViewID]*WebView | 52 | + | activeWebViews sync.Map // map[port.WebViewID]*WebView |
| 53 | - | activeCount atomic.Int32 | 53 | + | browserWebViews sync.Map // map[int32]*WebView |
| 54 | + | activeCount atomic.Int32 | |||
| 54 | 55 | ||||
| 55 | // shutdownNotify is signaled when the active webview count reaches 0 | 56 | // shutdownNotify is signaled when the active webview count reaches 0 | ||
| 56 | // during shutdown, replacing the busy-wait poll in closeActiveWebViews. | 57 | // during shutdown, replacing the busy-wait poll in closeActiveWebViews. | ||
| @@ -145,10 +146,29 @@ func (e *Engine) registerWebView(wv *WebView) { | |||||
| 145 | e.activeCount.Add(1) | 146 | e.activeCount.Add(1) | ||
| 146 | } | 147 | } | ||
| 147 | 148 | ||||
| 149 | + | func (e *Engine) bindBrowserWebView(browser purecef.Browser, wv *WebView) { | |||
| 150 | + | if e == nil || browser == nil || wv == nil { | |||
| 151 | + | return | |||
| 152 | + | } | |||
| 153 | + | e.browserWebViews.Store(browser.GetIdentifier(), wv) | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | func (e *Engine) unbindBrowserWebView(browserID int32, wv *WebView) { | |||
| 157 | + | if e == nil || browserID == 0 { | |||
| 158 | + | return | |||
| 159 | + | } | |||
| 160 | + | if current, ok := e.browserWebViews.Load(browserID); ok { | |||
| 161 | + | if existing, ok := current.(*WebView); ok && existing == wv { | |||
| 162 | + | e.browserWebViews.Delete(browserID) | |||
| 163 | + | } | |||
| 164 | + | } | |||
| 165 | + | } | |||
| 166 | + | ||||
| 148 | // unregisterWebView removes a webview from the active tracking map. | 167 | // unregisterWebView removes a webview from the active tracking map. | ||
| 149 | // If the count reaches 0 and a shutdown is in progress, it signals shutdownNotify. | 168 | // If the count reaches 0 and a shutdown is in progress, it signals shutdownNotify. | ||
| 150 | - | func (e *Engine) unregisterWebView(wv *WebView) { | 169 | + | func (e *Engine) unregisterWebView(wv *WebView, browserID int32) { |
| 151 | e.activeWebViews.Delete(wv.id) | 170 | e.activeWebViews.Delete(wv.id) | ||
| 171 | + | e.unbindBrowserWebView(browserID, wv) | |||
| 152 | if e.activeCount.Add(-1) == 0 && e.shutdownNotify != nil { | 172 | if e.activeCount.Add(-1) == 0 && e.shutdownNotify != nil { | ||
| 153 | select { | 173 | select { | ||
| 154 | case e.shutdownNotify <- struct{}{}: | 174 | case e.shutdownNotify <- struct{}{}: | ||
| @@ -432,28 +452,18 @@ func (e *Engine) validateBridgeRequest(browser purecef.Browser, bridgeNonce stri | |||||
| 432 | return false | 452 | return false | ||
| 433 | } | 453 | } | ||
| 434 | 454 | ||||
| 435 | - | browserID := browser.GetIdentifier() | 455 | + | wv := e.webViewForBrowser(browser) |
| 436 | - | valid := false | 456 | + | if wv == nil { |
| 437 | - | e.activeWebViews.Range(func(_, value any) bool { | 457 | + | return false |
| 438 | - | wv, ok := value.(*WebView) | 458 | + | } |
| 439 | - | if !ok || wv == nil { | |||
| 440 | - | return true | |||
| 441 | - | } | |||
| 442 | 459 | ||||
| 443 | - | wv.mu.RLock() | 460 | + | wv.mu.RLock() |
| 444 | - | wvBrowser := wv.browser | 461 | + | wvBridgeNonce := wv.bridgeNonce |
| 445 | - | wvBridgeNonce := wv.bridgeNonce | 462 | + | wv.mu.RUnlock() |
| 446 | - | wv.mu.RUnlock() | 463 | + | if wvBridgeNonce == "" { |
| 447 | - | if wvBrowser == nil || wvBridgeNonce == "" || wvBrowser.GetIdentifier() != browserID { | 464 | + | return false |
| 448 | - | return true | 465 | + | } |
| 449 | - | } | 466 | + | return subtle.ConstantTimeCompare([]byte(wvBridgeNonce), []byte(bridgeNonce)) == 1 |
| 450 | - | if subtle.ConstantTimeCompare([]byte(wvBridgeNonce), []byte(bridgeNonce)) == 1 { | |||
| 451 | - | valid = true | |||
| 452 | - | return false | |||
| 453 | - | } | |||
| 454 | - | return true | |||
| 455 | - | }) | |||
| 456 | - | return valid | |||
| 457 | } | 467 | } | ||
| 458 | 468 | ||||
| 459 | func (e *Engine) currentDownloadHandler() *downloadHandler { | 469 | func (e *Engine) currentDownloadHandler() *downloadHandler { | ||
M internal/infrastructure/cef/engine_popup_bridge.go +30 -29
| @@ -12,6 +12,18 @@ func (e *Engine) webViewForBrowser(browser purecef.Browser) *WebView { | |||
| 12 | 12 | } | |
| 13 | 13 | ||
| 14 | 14 | browserID := browser.GetIdentifier() | |
| 15 | + | if current, ok := e.browserWebViews.Load(browserID); ok { | |
| 16 | + | if wv, ok := current.(*WebView); ok && wv != nil { | |
| 17 | + | wv.mu.RLock() | |
| 18 | + | wvBrowser := wv.browser | |
| 19 | + | wv.mu.RUnlock() | |
| 20 | + | if wvBrowser != nil && wvBrowser.GetIdentifier() == browserID { | |
| 21 | + | return wv | |
| 22 | + | } | |
| 23 | + | e.unbindBrowserWebView(browserID, wv) | |
| 24 | + | } | |
| 25 | + | } | |
| 26 | + | ||
| 15 | 27 | var matched *WebView | |
| 16 | 28 | e.activeWebViews.Range(func(_, value any) bool { | |
| 17 | 29 | wv, ok := value.(*WebView) | |
| @@ -25,52 +37,41 @@ func (e *Engine) webViewForBrowser(browser purecef.Browser) *WebView { | |||
| 25 | 37 | return true | |
| 26 | 38 | } | |
| 27 | 39 | matched = wv | |
| 40 | + | e.bindBrowserWebView(browser, wv) | |
| 28 | 41 | return false | |
| 29 | 42 | }) | |
| 30 | 43 | return matched | |
| 31 | 44 | } | |
| 32 | 45 | ||
| 33 | - | func (e *Engine) handlePopupBridgeOpen(browser purecef.Browser, payload rendererBridgePopupOpenPayload) { | |
| 34 | - | if e == nil { | |
| 46 | + | func (e *Engine) withBridgeSourceWebView(browser purecef.Browser, proxyID, action string, fn func(*WebView)) { | |
| 47 | + | if e == nil || browser == nil { | |
| 35 | 48 | return | |
| 36 | 49 | } | |
| 37 | 50 | wv := e.webViewForBrowser(browser) | |
| 38 | 51 | if wv == nil { | |
| 39 | 52 | logging.FromContext(e.currentContext()).Warn(). | |
| 40 | 53 | Int32("browser_id", browser.GetIdentifier()). | |
| 41 | - | Str("proxy_id", payload.ProxyID). | |
| 42 | - | Msg("cef: popup-open bridge could not resolve source webview") | |
| 54 | + | Str("proxy_id", proxyID). | |
| 55 | + | Msg("cef: " + action + " bridge could not resolve source webview") | |
| 43 | 56 | return | |
| 44 | 57 | } | |
| 45 | - | wv.handleSyntheticPopupOpen(payload.URL, payload.FrameName, payload.ProxyID, payload.UserGesture, payload.NoJavaScriptAccess) | |
| 58 | + | fn(wv) | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | func (e *Engine) handlePopupBridgeOpen(browser purecef.Browser, payload rendererBridgePopupOpenPayload) { | |
| 62 | + | e.withBridgeSourceWebView(browser, payload.ProxyID, "popup-open", func(wv *WebView) { | |
| 63 | + | wv.handleSyntheticPopupOpen(payload.URL, payload.FrameName, payload.ProxyID, payload.UserGesture, payload.NoJavaScriptAccess) | |
| 64 | + | }) | |
| 46 | 65 | } | |
| 47 | 66 | ||
| 48 | 67 | func (e *Engine) handlePopupBridgeNavigate(browser purecef.Browser, payload rendererBridgePopupNavigatePayload) { | |
| 49 | - | if e == nil { | |
| 50 | - | return | |
| 51 | - | } | |
| 52 | - | wv := e.webViewForBrowser(browser) | |
| 53 | - | if wv == nil { | |
| 54 | - | logging.FromContext(e.currentContext()).Warn(). | |
| 55 | - | Int32("browser_id", browser.GetIdentifier()). | |
| 56 | - | Str("proxy_id", payload.ProxyID). | |
| 57 | - | Msg("cef: popup-navigate bridge could not resolve source webview") | |
| 58 | - | return | |
| 59 | - | } | |
| 60 | - | wv.handleSyntheticPopupNavigate(payload.ProxyID, payload.URL) | |
| 68 | + | e.withBridgeSourceWebView(browser, payload.ProxyID, "popup-navigate", func(wv *WebView) { | |
| 69 | + | wv.handleSyntheticPopupNavigate(payload.ProxyID, payload.URL) | |
| 70 | + | }) | |
| 61 | 71 | } | |
| 62 | 72 | ||
| 63 | 73 | func (e *Engine) handlePopupBridgeClose(browser purecef.Browser, payload rendererBridgePopupClosePayload) { | |
| 64 | - | if e == nil { | |
| 65 | - | return | |
| 66 | - | } | |
| 67 | - | wv := e.webViewForBrowser(browser) | |
| 68 | - | if wv == nil { | |
| 69 | - | logging.FromContext(e.currentContext()).Warn(). | |
| 70 | - | Int32("browser_id", browser.GetIdentifier()). | |
| 71 | - | Str("proxy_id", payload.ProxyID). | |
| 72 | - | Msg("cef: popup-close bridge could not resolve source webview") | |
| 73 | - | return | |
| 74 | - | } | |
| 75 | - | wv.handleSyntheticPopupClose(payload.ProxyID) | |
| 74 | + | e.withBridgeSourceWebView(browser, payload.ProxyID, "popup-close", func(wv *WebView) { | |
| 75 | + | wv.handleSyntheticPopupClose(payload.ProxyID) | |
| 76 | + | }) | |
| 76 | 77 | } | |
| @@ -12,6 +12,18 @@ func (e *Engine) webViewForBrowser(browser purecef.Browser) *WebView { | |||||
| 12 | } | 12 | } | ||
| 13 | 13 | ||||
| 14 | browserID := browser.GetIdentifier() | 14 | browserID := browser.GetIdentifier() | ||
| 15 | + | if current, ok := e.browserWebViews.Load(browserID); ok { | |||
| 16 | + | if wv, ok := current.(*WebView); ok && wv != nil { | |||
| 17 | + | wv.mu.RLock() | |||
| 18 | + | wvBrowser := wv.browser | |||
| 19 | + | wv.mu.RUnlock() | |||
| 20 | + | if wvBrowser != nil && wvBrowser.GetIdentifier() == browserID { | |||
| 21 | + | return wv | |||
| 22 | + | } | |||
| 23 | + | e.unbindBrowserWebView(browserID, wv) | |||
| 24 | + | } | |||
| 25 | + | } | |||
| 26 | + | ||||
| 15 | var matched *WebView | 27 | var matched *WebView | ||
| 16 | e.activeWebViews.Range(func(_, value any) bool { | 28 | e.activeWebViews.Range(func(_, value any) bool { | ||
| 17 | wv, ok := value.(*WebView) | 29 | wv, ok := value.(*WebView) | ||
| @@ -25,52 +37,41 @@ func (e *Engine) webViewForBrowser(browser purecef.Browser) *WebView { | |||||
| 25 | return true | 37 | return true | ||
| 26 | } | 38 | } | ||
| 27 | matched = wv | 39 | matched = wv | ||
| 40 | + | e.bindBrowserWebView(browser, wv) | |||
| 28 | return false | 41 | return false | ||
| 29 | }) | 42 | }) | ||
| 30 | return matched | 43 | return matched | ||
| 31 | } | 44 | } | ||
| 32 | 45 | ||||
| 33 | - | func (e *Engine) handlePopupBridgeOpen(browser purecef.Browser, payload rendererBridgePopupOpenPayload) { | 46 | + | func (e *Engine) withBridgeSourceWebView(browser purecef.Browser, proxyID, action string, fn func(*WebView)) { |
| 34 | - | if e == nil { | 47 | + | if e == nil || browser == nil { |
| 35 | return | 48 | return | ||
| 36 | } | 49 | } | ||
| 37 | wv := e.webViewForBrowser(browser) | 50 | wv := e.webViewForBrowser(browser) | ||
| 38 | if wv == nil { | 51 | if wv == nil { | ||
| 39 | logging.FromContext(e.currentContext()).Warn(). | 52 | logging.FromContext(e.currentContext()).Warn(). | ||
| 40 | Int32("browser_id", browser.GetIdentifier()). | 53 | Int32("browser_id", browser.GetIdentifier()). | ||
| 41 | - | Str("proxy_id", payload.ProxyID). | 54 | + | Str("proxy_id", proxyID). |
| 42 | - | Msg("cef: popup-open bridge could not resolve source webview") | 55 | + | Msg("cef: " + action + " bridge could not resolve source webview") |
| 43 | return | 56 | return | ||
| 44 | } | 57 | } | ||
| 45 | - | wv.handleSyntheticPopupOpen(payload.URL, payload.FrameName, payload.ProxyID, payload.UserGesture, payload.NoJavaScriptAccess) | 58 | + | fn(wv) |
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | func (e *Engine) handlePopupBridgeOpen(browser purecef.Browser, payload rendererBridgePopupOpenPayload) { | |||
| 62 | + | e.withBridgeSourceWebView(browser, payload.ProxyID, "popup-open", func(wv *WebView) { | |||
| 63 | + | wv.handleSyntheticPopupOpen(payload.URL, payload.FrameName, payload.ProxyID, payload.UserGesture, payload.NoJavaScriptAccess) | |||
| 64 | + | }) | |||
| 46 | } | 65 | } | ||
| 47 | 66 | ||||
| 48 | func (e *Engine) handlePopupBridgeNavigate(browser purecef.Browser, payload rendererBridgePopupNavigatePayload) { | 67 | func (e *Engine) handlePopupBridgeNavigate(browser purecef.Browser, payload rendererBridgePopupNavigatePayload) { | ||
| 49 | - | if e == nil { | 68 | + | e.withBridgeSourceWebView(browser, payload.ProxyID, "popup-navigate", func(wv *WebView) { |
| 50 | - | return | 69 | + | wv.handleSyntheticPopupNavigate(payload.ProxyID, payload.URL) |
| 51 | - | } | 70 | + | }) |
| 52 | - | wv := e.webViewForBrowser(browser) | |||
| 53 | - | if wv == nil { | |||
| 54 | - | logging.FromContext(e.currentContext()).Warn(). | |||
| 55 | - | Int32("browser_id", browser.GetIdentifier()). | |||
| 56 | - | Str("proxy_id", payload.ProxyID). | |||
| 57 | - | Msg("cef: popup-navigate bridge could not resolve source webview") | |||
| 58 | - | return | |||
| 59 | - | } | |||
| 60 | - | wv.handleSyntheticPopupNavigate(payload.ProxyID, payload.URL) | |||
| 61 | } | 71 | } | ||
| 62 | 72 | ||||
| 63 | func (e *Engine) handlePopupBridgeClose(browser purecef.Browser, payload rendererBridgePopupClosePayload) { | 73 | func (e *Engine) handlePopupBridgeClose(browser purecef.Browser, payload rendererBridgePopupClosePayload) { | ||
| 64 | - | if e == nil { | 74 | + | e.withBridgeSourceWebView(browser, payload.ProxyID, "popup-close", func(wv *WebView) { |
| 65 | - | return | 75 | + | wv.handleSyntheticPopupClose(payload.ProxyID) |
| 66 | - | } | 76 | + | }) |
| 67 | - | wv := e.webViewForBrowser(browser) | |||
| 68 | - | if wv == nil { | |||
| 69 | - | logging.FromContext(e.currentContext()).Warn(). | |||
| 70 | - | Int32("browser_id", browser.GetIdentifier()). | |||
| 71 | - | Str("proxy_id", payload.ProxyID). | |||
| 72 | - | Msg("cef: popup-close bridge could not resolve source webview") | |||
| 73 | - | return | |||
| 74 | - | } | |||
| 75 | - | wv.handleSyntheticPopupClose(payload.ProxyID) | |||
| 76 | } | 77 | } | ||
M internal/infrastructure/cef/renderer_bridge.go +15 -25
| @@ -499,46 +499,36 @@ func decodeRendererBridgeExplicitTextCopyPayload(payload []byte) (rendererBridge | |||
| 499 | 499 | return req, nil | |
| 500 | 500 | } | |
| 501 | 501 | ||
| 502 | - | func decodeRendererBridgePopupOpenPayload(payload []byte) (rendererBridgePopupOpenPayload, error) { | |
| 503 | - | var req rendererBridgePopupOpenPayload | |
| 502 | + | func decodeRendererBridgePopupPayload[T any](payload []byte, proxyID func(T) string) (T, error) { | |
| 503 | + | var req T | |
| 504 | 504 | if len(payload) == 0 { | |
| 505 | 505 | return req, fmt.Errorf("empty payload") | |
| 506 | 506 | } | |
| 507 | 507 | if err := json.Unmarshal(payload, &req); err != nil { | |
| 508 | 508 | return req, err | |
| 509 | 509 | } | |
| 510 | - | if req.ProxyID == "" { | |
| 510 | + | if proxyID(req) == "" { | |
| 511 | 511 | return req, fmt.Errorf("missing proxy_id") | |
| 512 | 512 | } | |
| 513 | 513 | return req, nil | |
| 514 | 514 | } | |
| 515 | 515 | ||
| 516 | + | func decodeRendererBridgePopupOpenPayload(payload []byte) (rendererBridgePopupOpenPayload, error) { | |
| 517 | + | return decodeRendererBridgePopupPayload(payload, func(req rendererBridgePopupOpenPayload) string { | |
| 518 | + | return req.ProxyID | |
| 519 | + | }) | |
| 520 | + | } | |
| 521 | + | ||
| 516 | 522 | func decodeRendererBridgePopupNavigatePayload(payload []byte) (rendererBridgePopupNavigatePayload, error) { | |
| 517 | - | var req rendererBridgePopupNavigatePayload | |
| 518 | - | if len(payload) == 0 { | |
| 519 | - | return req, fmt.Errorf("empty payload") | |
| 520 | - | } | |
| 521 | - | if err := json.Unmarshal(payload, &req); err != nil { | |
| 522 | - | return req, err | |
| 523 | - | } | |
| 524 | - | if req.ProxyID == "" { | |
| 525 | - | return req, fmt.Errorf("missing proxy_id") | |
| 526 | - | } | |
| 527 | - | return req, nil | |
| 523 | + | return decodeRendererBridgePopupPayload(payload, func(req rendererBridgePopupNavigatePayload) string { | |
| 524 | + | return req.ProxyID | |
| 525 | + | }) | |
| 528 | 526 | } | |
| 529 | 527 | ||
| 530 | 528 | func decodeRendererBridgePopupClosePayload(payload []byte) (rendererBridgePopupClosePayload, error) { | |
| 531 | - | var req rendererBridgePopupClosePayload | |
| 532 | - | if len(payload) == 0 { | |
| 533 | - | return req, fmt.Errorf("empty payload") | |
| 534 | - | } | |
| 535 | - | if err := json.Unmarshal(payload, &req); err != nil { | |
| 536 | - | return req, err | |
| 537 | - | } | |
| 538 | - | if req.ProxyID == "" { | |
| 539 | - | return req, fmt.Errorf("missing proxy_id") | |
| 540 | - | } | |
| 541 | - | return req, nil | |
| 529 | + | return decodeRendererBridgePopupPayload(payload, func(req rendererBridgePopupClosePayload) string { | |
| 530 | + | return req.ProxyID | |
| 531 | + | }) | |
| 542 | 532 | } | |
| 543 | 533 | ||
| 544 | 534 | func (e *Engine) handleEditableFocusBridge(browser purecef.Browser) { | |
| @@ -499,46 +499,36 @@ func decodeRendererBridgeExplicitTextCopyPayload(payload []byte) (rendererBridge | |||||
| 499 | return req, nil | 499 | return req, nil | ||
| 500 | } | 500 | } | ||
| 501 | 501 | ||||
| 502 | - | func decodeRendererBridgePopupOpenPayload(payload []byte) (rendererBridgePopupOpenPayload, error) { | 502 | + | func decodeRendererBridgePopupPayload[T any](payload []byte, proxyID func(T) string) (T, error) { |
| 503 | - | var req rendererBridgePopupOpenPayload | 503 | + | var req T |
| 504 | if len(payload) == 0 { | 504 | if len(payload) == 0 { | ||
| 505 | return req, fmt.Errorf("empty payload") | 505 | return req, fmt.Errorf("empty payload") | ||
| 506 | } | 506 | } | ||
| 507 | if err := json.Unmarshal(payload, &req); err != nil { | 507 | if err := json.Unmarshal(payload, &req); err != nil { | ||
| 508 | return req, err | 508 | return req, err | ||
| 509 | } | 509 | } | ||
| 510 | - | if req.ProxyID == "" { | 510 | + | if proxyID(req) == "" { |
| 511 | return req, fmt.Errorf("missing proxy_id") | 511 | return req, fmt.Errorf("missing proxy_id") | ||
| 512 | } | 512 | } | ||
| 513 | return req, nil | 513 | return req, nil | ||
| 514 | } | 514 | } | ||
| 515 | 515 | ||||
| 516 | + | func decodeRendererBridgePopupOpenPayload(payload []byte) (rendererBridgePopupOpenPayload, error) { | |||
| 517 | + | return decodeRendererBridgePopupPayload(payload, func(req rendererBridgePopupOpenPayload) string { | |||
| 518 | + | return req.ProxyID | |||
| 519 | + | }) | |||
| 520 | + | } | |||
| 521 | + | ||||
| 516 | func decodeRendererBridgePopupNavigatePayload(payload []byte) (rendererBridgePopupNavigatePayload, error) { | 522 | func decodeRendererBridgePopupNavigatePayload(payload []byte) (rendererBridgePopupNavigatePayload, error) { | ||
| 517 | - | var req rendererBridgePopupNavigatePayload | 523 | + | return decodeRendererBridgePopupPayload(payload, func(req rendererBridgePopupNavigatePayload) string { |
| 518 | - | if len(payload) == 0 { | 524 | + | return req.ProxyID |
| 519 | - | return req, fmt.Errorf("empty payload") | 525 | + | }) |
| 520 | - | } | |||
| 521 | - | if err := json.Unmarshal(payload, &req); err != nil { | |||
| 522 | - | return req, err | |||
| 523 | - | } | |||
| 524 | - | if req.ProxyID == "" { | |||
| 525 | - | return req, fmt.Errorf("missing proxy_id") | |||
| 526 | - | } | |||
| 527 | - | return req, nil | |||
| 528 | } | 526 | } | ||
| 529 | 527 | ||||
| 530 | func decodeRendererBridgePopupClosePayload(payload []byte) (rendererBridgePopupClosePayload, error) { | 528 | func decodeRendererBridgePopupClosePayload(payload []byte) (rendererBridgePopupClosePayload, error) { | ||
| 531 | - | var req rendererBridgePopupClosePayload | 529 | + | return decodeRendererBridgePopupPayload(payload, func(req rendererBridgePopupClosePayload) string { |
| 532 | - | if len(payload) == 0 { | 530 | + | return req.ProxyID |
| 533 | - | return req, fmt.Errorf("empty payload") | 531 | + | }) |
| 534 | - | } | |||
| 535 | - | if err := json.Unmarshal(payload, &req); err != nil { | |||
| 536 | - | return req, err | |||
| 537 | - | } | |||
| 538 | - | if req.ProxyID == "" { | |||
| 539 | - | return req, fmt.Errorf("missing proxy_id") | |||
| 540 | - | } | |||
| 541 | - | return req, nil | |||
| 542 | } | 532 | } | ||
| 543 | 533 | ||||
| 544 | func (e *Engine) handleEditableFocusBridge(browser purecef.Browser) { | 534 | func (e *Engine) handleEditableFocusBridge(browser purecef.Browser) { | ||
M internal/infrastructure/cef/runtime_config.go +1 -1
| @@ -9,7 +9,7 @@ import ( | |||
| 9 | 9 | ||
| 10 | 10 | var ( | |
| 11 | 11 | ErrDownloadsUnsupported = errors.New("cef: downloads are not supported yet") | |
| 12 | - | ErrRelatedWebViewUnsupported = errors.New("cef: related popup webviews are not supported yet") | |
| 12 | + | ErrRelatedWebViewUnsupported = port.ErrRelatedWebViewUnsupported | |
| 13 | 13 | ErrCookiePolicyUnsupported = errors.New("cef: non-default cookie policy is not supported yet") | |
| 14 | 14 | ) | |
| 15 | 15 | ||
| @@ -9,7 +9,7 @@ import ( | |||||
| 9 | 9 | ||||
| 10 | var ( | 10 | var ( | ||
| 11 | ErrDownloadsUnsupported = errors.New("cef: downloads are not supported yet") | 11 | ErrDownloadsUnsupported = errors.New("cef: downloads are not supported yet") | ||
| 12 | - | ErrRelatedWebViewUnsupported = errors.New("cef: related popup webviews are not supported yet") | 12 | + | ErrRelatedWebViewUnsupported = port.ErrRelatedWebViewUnsupported |
| 13 | ErrCookiePolicyUnsupported = errors.New("cef: non-default cookie policy is not supported yet") | 13 | ErrCookiePolicyUnsupported = errors.New("cef: non-default cookie policy is not supported yet") | ||
| 14 | ) | 14 | ) | ||
| 15 | 15 | ||||
M internal/infrastructure/cef/scheme_handler.go +20 -51
| @@ -348,79 +348,48 @@ func (h *dumbSchemeHandler) handleFocusSync(request purecef.Request, browser pur | |||
| 348 | 348 | return h.newAPIJSONResourceHandler(http.StatusOK, map[string]any{"ok": true}) | |
| 349 | 349 | } | |
| 350 | 350 | ||
| 351 | - | func (h *dumbSchemeHandler) handlePopupOpen(request purecef.Request, browser purecef.Browser) purecef.ResourceHandler { | |
| 351 | + | func handlePopupBridgeRequest[T any]( | |
| 352 | + | h *dumbSchemeHandler, | |
| 353 | + | request purecef.Request, | |
| 354 | + | browser purecef.Browser, | |
| 355 | + | action string, | |
| 356 | + | decode func([]byte) (T, error), | |
| 357 | + | dispatch func(purecef.Browser, T), | |
| 358 | + | ) purecef.ResourceHandler { | |
| 352 | 359 | if browser == nil { | |
| 353 | - | h.logger.Debug().Msg("cef: popup-open — browser unavailable") | |
| 360 | + | h.logger.Debug().Msg("cef: " + action + " — browser unavailable") | |
| 354 | 361 | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "browser unavailable"}) | |
| 355 | 362 | } | |
| 356 | 363 | if !h.hasTrustedBridgeNonce(request, browser) { | |
| 357 | - | h.logger.Warn().Msg("cef: popup-open — rejected request without valid bridge nonce") | |
| 364 | + | h.logger.Warn().Msg("cef: " + action + " — rejected request without valid bridge nonce") | |
| 358 | 365 | return h.newAPIJSONResourceHandler(http.StatusForbidden, map[string]string{"error": "forbidden"}) | |
| 359 | 366 | } | |
| 360 | 367 | body := readBodyFromHeader(request) | |
| 361 | 368 | if body == nil { | |
| 362 | 369 | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "empty body"}) | |
| 363 | 370 | } | |
| 364 | - | payload, err := decodeRendererBridgePopupOpenPayload(body) | |
| 371 | + | payload, err := decode(body) | |
| 365 | 372 | if err != nil { | |
| 366 | 373 | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": err.Error()}) | |
| 367 | 374 | } | |
| 368 | - | if h.onPopupOpen != nil { | |
| 369 | - | h.onPopupOpen(browser, payload) | |
| 375 | + | if dispatch != nil { | |
| 376 | + | dispatch(browser, payload) | |
| 370 | 377 | } else { | |
| 371 | - | h.logger.Warn().Msg("cef: popup-open — callback not wired") | |
| 378 | + | h.logger.Warn().Msg("cef: " + action + " — callback not wired") | |
| 372 | 379 | } | |
| 373 | 380 | return h.newAPIJSONResourceHandler(http.StatusOK, map[string]any{"ok": true}) | |
| 374 | 381 | } | |
| 375 | 382 | ||
| 383 | + | func (h *dumbSchemeHandler) handlePopupOpen(request purecef.Request, browser purecef.Browser) purecef.ResourceHandler { | |
| 384 | + | return handlePopupBridgeRequest(h, request, browser, "popup-open", decodeRendererBridgePopupOpenPayload, h.onPopupOpen) | |
| 385 | + | } | |
| 386 | + | ||
| 376 | 387 | func (h *dumbSchemeHandler) handlePopupNavigate(request purecef.Request, browser purecef.Browser) purecef.ResourceHandler { | |
| 377 | - | if browser == nil { | |
| 378 | - | h.logger.Debug().Msg("cef: popup-navigate — browser unavailable") | |
| 379 | - | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "browser unavailable"}) | |
| 380 | - | } | |
| 381 | - | if !h.hasTrustedBridgeNonce(request, browser) { | |
| 382 | - | h.logger.Warn().Msg("cef: popup-navigate — rejected request without valid bridge nonce") | |
| 383 | - | return h.newAPIJSONResourceHandler(http.StatusForbidden, map[string]string{"error": "forbidden"}) | |
| 384 | - | } | |
| 385 | - | body := readBodyFromHeader(request) | |
| 386 | - | if body == nil { | |
| 387 | - | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "empty body"}) | |
| 388 | - | } | |
| 389 | - | payload, err := decodeRendererBridgePopupNavigatePayload(body) | |
| 390 | - | if err != nil { | |
| 391 | - | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": err.Error()}) | |
| 392 | - | } | |
| 393 | - | if h.onPopupNavigate != nil { | |
| 394 | - | h.onPopupNavigate(browser, payload) | |
| 395 | - | } else { | |
| 396 | - | h.logger.Warn().Msg("cef: popup-navigate — callback not wired") | |
| 397 | - | } | |
| 398 | - | return h.newAPIJSONResourceHandler(http.StatusOK, map[string]any{"ok": true}) | |
| 388 | + | return handlePopupBridgeRequest(h, request, browser, "popup-navigate", decodeRendererBridgePopupNavigatePayload, h.onPopupNavigate) | |
| 399 | 389 | } | |
| 400 | 390 | ||
| 401 | 391 | func (h *dumbSchemeHandler) handlePopupClose(request purecef.Request, browser purecef.Browser) purecef.ResourceHandler { | |
| 402 | - | if browser == nil { | |
| 403 | - | h.logger.Debug().Msg("cef: popup-close — browser unavailable") | |
| 404 | - | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "browser unavailable"}) | |
| 405 | - | } | |
| 406 | - | if !h.hasTrustedBridgeNonce(request, browser) { | |
| 407 | - | h.logger.Warn().Msg("cef: popup-close — rejected request without valid bridge nonce") | |
| 408 | - | return h.newAPIJSONResourceHandler(http.StatusForbidden, map[string]string{"error": "forbidden"}) | |
| 409 | - | } | |
| 410 | - | body := readBodyFromHeader(request) | |
| 411 | - | if body == nil { | |
| 412 | - | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "empty body"}) | |
| 413 | - | } | |
| 414 | - | payload, err := decodeRendererBridgePopupClosePayload(body) | |
| 415 | - | if err != nil { | |
| 416 | - | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": err.Error()}) | |
| 417 | - | } | |
| 418 | - | if h.onPopupClose != nil { | |
| 419 | - | h.onPopupClose(browser, payload) | |
| 420 | - | } else { | |
| 421 | - | h.logger.Warn().Msg("cef: popup-close — callback not wired") | |
| 422 | - | } | |
| 423 | - | return h.newAPIJSONResourceHandler(http.StatusOK, map[string]any{"ok": true}) | |
| 392 | + | return handlePopupBridgeRequest(h, request, browser, "popup-close", decodeRendererBridgePopupClosePayload, h.onPopupClose) | |
| 424 | 393 | } | |
| 425 | 394 | ||
| 426 | 395 | func (h *dumbSchemeHandler) hasTrustedBridgeNonce(request purecef.Request, browser purecef.Browser) bool { | |
| @@ -348,79 +348,48 @@ func (h *dumbSchemeHandler) handleFocusSync(request purecef.Request, browser pur | |||||
| 348 | return h.newAPIJSONResourceHandler(http.StatusOK, map[string]any{"ok": true}) | 348 | return h.newAPIJSONResourceHandler(http.StatusOK, map[string]any{"ok": true}) | ||
| 349 | } | 349 | } | ||
| 350 | 350 | ||||
| 351 | - | func (h *dumbSchemeHandler) handlePopupOpen(request purecef.Request, browser purecef.Browser) purecef.ResourceHandler { | 351 | + | func handlePopupBridgeRequest[T any]( |
| 352 | + | h *dumbSchemeHandler, | |||
| 353 | + | request purecef.Request, | |||
| 354 | + | browser purecef.Browser, | |||
| 355 | + | action string, | |||
| 356 | + | decode func([]byte) (T, error), | |||
| 357 | + | dispatch func(purecef.Browser, T), | |||
| 358 | + | ) purecef.ResourceHandler { | |||
| 352 | if browser == nil { | 359 | if browser == nil { | ||
| 353 | - | h.logger.Debug().Msg("cef: popup-open — browser unavailable") | 360 | + | h.logger.Debug().Msg("cef: " + action + " — browser unavailable") |
| 354 | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "browser unavailable"}) | 361 | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "browser unavailable"}) | ||
| 355 | } | 362 | } | ||
| 356 | if !h.hasTrustedBridgeNonce(request, browser) { | 363 | if !h.hasTrustedBridgeNonce(request, browser) { | ||
| 357 | - | h.logger.Warn().Msg("cef: popup-open — rejected request without valid bridge nonce") | 364 | + | h.logger.Warn().Msg("cef: " + action + " — rejected request without valid bridge nonce") |
| 358 | return h.newAPIJSONResourceHandler(http.StatusForbidden, map[string]string{"error": "forbidden"}) | 365 | return h.newAPIJSONResourceHandler(http.StatusForbidden, map[string]string{"error": "forbidden"}) | ||
| 359 | } | 366 | } | ||
| 360 | body := readBodyFromHeader(request) | 367 | body := readBodyFromHeader(request) | ||
| 361 | if body == nil { | 368 | if body == nil { | ||
| 362 | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "empty body"}) | 369 | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "empty body"}) | ||
| 363 | } | 370 | } | ||
| 364 | - | payload, err := decodeRendererBridgePopupOpenPayload(body) | 371 | + | payload, err := decode(body) |
| 365 | if err != nil { | 372 | if err != nil { | ||
| 366 | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": err.Error()}) | 373 | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": err.Error()}) | ||
| 367 | } | 374 | } | ||
| 368 | - | if h.onPopupOpen != nil { | 375 | + | if dispatch != nil { |
| 369 | - | h.onPopupOpen(browser, payload) | 376 | + | dispatch(browser, payload) |
| 370 | } else { | 377 | } else { | ||
| 371 | - | h.logger.Warn().Msg("cef: popup-open — callback not wired") | 378 | + | h.logger.Warn().Msg("cef: " + action + " — callback not wired") |
| 372 | } | 379 | } | ||
| 373 | return h.newAPIJSONResourceHandler(http.StatusOK, map[string]any{"ok": true}) | 380 | return h.newAPIJSONResourceHandler(http.StatusOK, map[string]any{"ok": true}) | ||
| 374 | } | 381 | } | ||
| 375 | 382 | ||||
| 383 | + | func (h *dumbSchemeHandler) handlePopupOpen(request purecef.Request, browser purecef.Browser) purecef.ResourceHandler { | |||
| 384 | + | return handlePopupBridgeRequest(h, request, browser, "popup-open", decodeRendererBridgePopupOpenPayload, h.onPopupOpen) | |||
| 385 | + | } | |||
| 386 | + | ||||
| 376 | func (h *dumbSchemeHandler) handlePopupNavigate(request purecef.Request, browser purecef.Browser) purecef.ResourceHandler { | 387 | func (h *dumbSchemeHandler) handlePopupNavigate(request purecef.Request, browser purecef.Browser) purecef.ResourceHandler { | ||
| 377 | - | if browser == nil { | 388 | + | return handlePopupBridgeRequest(h, request, browser, "popup-navigate", decodeRendererBridgePopupNavigatePayload, h.onPopupNavigate) |
| 378 | - | h.logger.Debug().Msg("cef: popup-navigate — browser unavailable") | |||
| 379 | - | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "browser unavailable"}) | |||
| 380 | - | } | |||
| 381 | - | if !h.hasTrustedBridgeNonce(request, browser) { | |||
| 382 | - | h.logger.Warn().Msg("cef: popup-navigate — rejected request without valid bridge nonce") | |||
| 383 | - | return h.newAPIJSONResourceHandler(http.StatusForbidden, map[string]string{"error": "forbidden"}) | |||
| 384 | - | } | |||
| 385 | - | body := readBodyFromHeader(request) | |||
| 386 | - | if body == nil { | |||
| 387 | - | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "empty body"}) | |||
| 388 | - | } | |||
| 389 | - | payload, err := decodeRendererBridgePopupNavigatePayload(body) | |||
| 390 | - | if err != nil { | |||
| 391 | - | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": err.Error()}) | |||
| 392 | - | } | |||
| 393 | - | if h.onPopupNavigate != nil { | |||
| 394 | - | h.onPopupNavigate(browser, payload) | |||
| 395 | - | } else { | |||
| 396 | - | h.logger.Warn().Msg("cef: popup-navigate — callback not wired") | |||
| 397 | - | } | |||
| 398 | - | return h.newAPIJSONResourceHandler(http.StatusOK, map[string]any{"ok": true}) | |||
| 399 | } | 389 | } | ||
| 400 | 390 | ||||
| 401 | func (h *dumbSchemeHandler) handlePopupClose(request purecef.Request, browser purecef.Browser) purecef.ResourceHandler { | 391 | func (h *dumbSchemeHandler) handlePopupClose(request purecef.Request, browser purecef.Browser) purecef.ResourceHandler { | ||
| 402 | - | if browser == nil { | 392 | + | return handlePopupBridgeRequest(h, request, browser, "popup-close", decodeRendererBridgePopupClosePayload, h.onPopupClose) |
| 403 | - | h.logger.Debug().Msg("cef: popup-close — browser unavailable") | |||
| 404 | - | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "browser unavailable"}) | |||
| 405 | - | } | |||
| 406 | - | if !h.hasTrustedBridgeNonce(request, browser) { | |||
| 407 | - | h.logger.Warn().Msg("cef: popup-close — rejected request without valid bridge nonce") | |||
| 408 | - | return h.newAPIJSONResourceHandler(http.StatusForbidden, map[string]string{"error": "forbidden"}) | |||
| 409 | - | } | |||
| 410 | - | body := readBodyFromHeader(request) | |||
| 411 | - | if body == nil { | |||
| 412 | - | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "empty body"}) | |||
| 413 | - | } | |||
| 414 | - | payload, err := decodeRendererBridgePopupClosePayload(body) | |||
| 415 | - | if err != nil { | |||
| 416 | - | return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": err.Error()}) | |||
| 417 | - | } | |||
| 418 | - | if h.onPopupClose != nil { | |||
| 419 | - | h.onPopupClose(browser, payload) | |||
| 420 | - | } else { | |||
| 421 | - | h.logger.Warn().Msg("cef: popup-close — callback not wired") | |||
| 422 | - | } | |||
| 423 | - | return h.newAPIJSONResourceHandler(http.StatusOK, map[string]any{"ok": true}) | |||
| 424 | } | 393 | } | ||
| 425 | 394 | ||||
| 426 | func (h *dumbSchemeHandler) hasTrustedBridgeNonce(request purecef.Request, browser purecef.Browser) bool { | 395 | func (h *dumbSchemeHandler) hasTrustedBridgeNonce(request purecef.Request, browser purecef.Browser) bool { | ||
M internal/infrastructure/cef/webview_handlers.go +7 -2
| @@ -730,6 +730,7 @@ func (h *handlerSet) OnAfterCreated(browser purecef.Browser) { | |||
| 730 | 730 | if h.wv.engine != nil { | |
| 731 | 731 | h.wv.engine.recordBrowserAfterCreated(browser) | |
| 732 | 732 | h.wv.engine.registerWebView(h.wv) | |
| 733 | + | h.wv.engine.bindBrowserWebView(browser, h.wv) | |
| 733 | 734 | } | |
| 734 | 735 | host := browser.GetHost() | |
| 735 | 736 | if host == nil { | |
| @@ -783,9 +784,13 @@ func (h *handlerSet) DoClose(_ purecef.Browser) bool { | |||
| 783 | 784 | } | |
| 784 | 785 | ||
| 785 | 786 | // OnBeforeClose fires the OnClose callback. | |
| 786 | - | func (h *handlerSet) OnBeforeClose(_ purecef.Browser) { | |
| 787 | + | func (h *handlerSet) OnBeforeClose(browser purecef.Browser) { | |
| 787 | 788 | if h.wv.engine != nil { | |
| 788 | - | h.wv.engine.unregisterWebView(h.wv) | |
| 789 | + | browserID := int32(0) | |
| 790 | + | if browser != nil { | |
| 791 | + | browserID = browser.GetIdentifier() | |
| 792 | + | } | |
| 793 | + | h.wv.engine.unregisterWebView(h.wv, browserID) | |
| 789 | 794 | } | |
| 790 | 795 | h.wv.mu.Lock() | |
| 791 | 796 | h.wv.browser = nil | |
| @@ -730,6 +730,7 @@ func (h *handlerSet) OnAfterCreated(browser purecef.Browser) { | |||||
| 730 | if h.wv.engine != nil { | 730 | if h.wv.engine != nil { | ||
| 731 | h.wv.engine.recordBrowserAfterCreated(browser) | 731 | h.wv.engine.recordBrowserAfterCreated(browser) | ||
| 732 | h.wv.engine.registerWebView(h.wv) | 732 | h.wv.engine.registerWebView(h.wv) | ||
| 733 | + | h.wv.engine.bindBrowserWebView(browser, h.wv) | |||
| 733 | } | 734 | } | ||
| 734 | host := browser.GetHost() | 735 | host := browser.GetHost() | ||
| 735 | if host == nil { | 736 | if host == nil { | ||
| @@ -783,9 +784,13 @@ func (h *handlerSet) DoClose(_ purecef.Browser) bool { | |||||
| 783 | } | 784 | } | ||
| 784 | 785 | ||||
| 785 | // OnBeforeClose fires the OnClose callback. | 786 | // OnBeforeClose fires the OnClose callback. | ||
| 786 | - | func (h *handlerSet) OnBeforeClose(_ purecef.Browser) { | 787 | + | func (h *handlerSet) OnBeforeClose(browser purecef.Browser) { |
| 787 | if h.wv.engine != nil { | 788 | if h.wv.engine != nil { | ||
| 788 | - | h.wv.engine.unregisterWebView(h.wv) | 789 | + | browserID := int32(0) |
| 790 | + | if browser != nil { | |||
| 791 | + | browserID = browser.GetIdentifier() | |||
| 792 | + | } | |||
| 793 | + | h.wv.engine.unregisterWebView(h.wv, browserID) | |||
| 789 | } | 794 | } | ||
| 790 | h.wv.mu.Lock() | 795 | h.wv.mu.Lock() | ||
| 791 | h.wv.browser = nil | 796 | h.wv.browser = nil | ||
M internal/infrastructure/cef/webview_popup_proxy.go +23 -1
| @@ -33,12 +33,32 @@ func (wv *WebView) syntheticPopupState(proxyID string) *syntheticPopupState { | |||
| 33 | 33 | return state | |
| 34 | 34 | } | |
| 35 | 35 | ||
| 36 | + | func (wv *WebView) lookupSyntheticPopupState(proxyID string) *syntheticPopupState { | |
| 37 | + | if wv == nil || proxyID == "" { | |
| 38 | + | return nil | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | wv.syntheticPopupMu.Lock() | |
| 42 | + | defer wv.syntheticPopupMu.Unlock() | |
| 43 | + | return wv.syntheticPopups[proxyID] | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | func (wv *WebView) deleteSyntheticPopupState(proxyID string) { | |
| 47 | + | if wv == nil || proxyID == "" { | |
| 48 | + | return | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | wv.syntheticPopupMu.Lock() | |
| 52 | + | defer wv.syntheticPopupMu.Unlock() | |
| 53 | + | delete(wv.syntheticPopups, proxyID) | |
| 54 | + | } | |
| 55 | + | ||
| 36 | 56 | func (wv *WebView) handleSyntheticPopupOpen(targetURL, frameName, proxyID string, userGesture, noJavaScriptAccess bool) { | |
| 37 | 57 | if wv == nil || proxyID == "" || wv.destroyed.Load() { | |
| 38 | 58 | return | |
| 39 | 59 | } | |
| 40 | 60 | ||
| 41 | - | state := wv.syntheticPopupState(proxyID) | |
| 61 | + | state := wv.lookupSyntheticPopupState(proxyID) | |
| 42 | 62 | if state != nil { | |
| 43 | 63 | wv.syntheticPopupMu.Lock() | |
| 44 | 64 | state.NoJavaScriptAccess = noJavaScriptAccess | |
| @@ -54,6 +74,7 @@ func (wv *WebView) handleSyntheticPopupOpen(targetURL, frameName, proxyID string | |||
| 54 | 74 | cb := wv.callbacks | |
| 55 | 75 | wv.mu.RUnlock() | |
| 56 | 76 | if cb == nil || cb.OnCreate == nil { | |
| 77 | + | wv.deleteSyntheticPopupState(proxyID) | |
| 57 | 78 | return | |
| 58 | 79 | } | |
| 59 | 80 | ||
| @@ -65,6 +86,7 @@ func (wv *WebView) handleSyntheticPopupOpen(targetURL, frameName, proxyID string | |||
| 65 | 86 | ParentViewID: wv.id, | |
| 66 | 87 | }) | |
| 67 | 88 | if popupWV == nil { | |
| 89 | + | wv.deleteSyntheticPopupState(proxyID) | |
| 68 | 90 | return | |
| 69 | 91 | } | |
| 70 | 92 | ||
| @@ -33,12 +33,32 @@ func (wv *WebView) syntheticPopupState(proxyID string) *syntheticPopupState { | |||||
| 33 | return state | 33 | return state | ||
| 34 | } | 34 | } | ||
| 35 | 35 | ||||
| 36 | + | func (wv *WebView) lookupSyntheticPopupState(proxyID string) *syntheticPopupState { | |||
| 37 | + | if wv == nil || proxyID == "" { | |||
| 38 | + | return nil | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | wv.syntheticPopupMu.Lock() | |||
| 42 | + | defer wv.syntheticPopupMu.Unlock() | |||
| 43 | + | return wv.syntheticPopups[proxyID] | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | func (wv *WebView) deleteSyntheticPopupState(proxyID string) { | |||
| 47 | + | if wv == nil || proxyID == "" { | |||
| 48 | + | return | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | wv.syntheticPopupMu.Lock() | |||
| 52 | + | defer wv.syntheticPopupMu.Unlock() | |||
| 53 | + | delete(wv.syntheticPopups, proxyID) | |||
| 54 | + | } | |||
| 55 | + | ||||
| 36 | func (wv *WebView) handleSyntheticPopupOpen(targetURL, frameName, proxyID string, userGesture, noJavaScriptAccess bool) { | 56 | func (wv *WebView) handleSyntheticPopupOpen(targetURL, frameName, proxyID string, userGesture, noJavaScriptAccess bool) { | ||
| 37 | if wv == nil || proxyID == "" || wv.destroyed.Load() { | 57 | if wv == nil || proxyID == "" || wv.destroyed.Load() { | ||
| 38 | return | 58 | return | ||
| 39 | } | 59 | } | ||
| 40 | 60 | ||||
| 41 | - | state := wv.syntheticPopupState(proxyID) | 61 | + | state := wv.lookupSyntheticPopupState(proxyID) |
| 42 | if state != nil { | 62 | if state != nil { | ||
| 43 | wv.syntheticPopupMu.Lock() | 63 | wv.syntheticPopupMu.Lock() | ||
| 44 | state.NoJavaScriptAccess = noJavaScriptAccess | 64 | state.NoJavaScriptAccess = noJavaScriptAccess | ||
| @@ -54,6 +74,7 @@ func (wv *WebView) handleSyntheticPopupOpen(targetURL, frameName, proxyID string | |||||
| 54 | cb := wv.callbacks | 74 | cb := wv.callbacks | ||
| 55 | wv.mu.RUnlock() | 75 | wv.mu.RUnlock() | ||
| 56 | if cb == nil || cb.OnCreate == nil { | 76 | if cb == nil || cb.OnCreate == nil { | ||
| 77 | + | wv.deleteSyntheticPopupState(proxyID) | |||
| 57 | return | 78 | return | ||
| 58 | } | 79 | } | ||
| 59 | 80 | ||||
| @@ -65,6 +86,7 @@ func (wv *WebView) handleSyntheticPopupOpen(targetURL, frameName, proxyID string | |||||
| 65 | ParentViewID: wv.id, | 86 | ParentViewID: wv.id, | ||
| 66 | }) | 87 | }) | ||
| 67 | if popupWV == nil { | 88 | if popupWV == nil { | ||
| 89 | + | wv.deleteSyntheticPopupState(proxyID) | |||
| 68 | return | 90 | return | ||
| 69 | } | 91 | } | ||
| 70 | 92 | ||||
M internal/infrastructure/cef/webview_popup_proxy_test.go +17 -0
| @@ -66,6 +66,23 @@ func TestHandleSyntheticPopupNavigate_LoadsMappedPopup(t *testing.T) { | |||
| 66 | 66 | parentWV.handleSyntheticPopupNavigate("popup-2", "https://example.com/finish") | |
| 67 | 67 | } | |
| 68 | 68 | ||
| 69 | + | func TestHandleSyntheticPopupOpen_CleansUpStateWhenPopupIsBlocked(t *testing.T) { | |
| 70 | + | parentWV := &WebView{ | |
| 71 | + | ctx: context.Background(), | |
| 72 | + | id: port.WebViewID(28), | |
| 73 | + | callbacks: &port.WebViewCallbacks{ | |
| 74 | + | OnCreate: func(port.PopupRequest) port.WebView { | |
| 75 | + | return nil | |
| 76 | + | }, | |
| 77 | + | }, | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | parentWV.handleSyntheticPopupNavigate("popup-blocked", "https://example.com/blocked") | |
| 81 | + | parentWV.handleSyntheticPopupOpen("https://example.com/blocked", "_blank", "popup-blocked", true, false) | |
| 82 | + | ||
| 83 | + | require.Empty(t, parentWV.syntheticPopups) | |
| 84 | + | } | |
| 85 | + | ||
| 69 | 86 | func TestHandleSyntheticPopupClose_ClosesMappedPopup(t *testing.T) { | |
| 70 | 87 | popupWV := &closeablePopupWebViewStub{MockWebView: portmocks.NewMockWebView(t)} | |
| 71 | 88 | parentWV := &WebView{ | |
| @@ -66,6 +66,23 @@ func TestHandleSyntheticPopupNavigate_LoadsMappedPopup(t *testing.T) { | |||||
| 66 | parentWV.handleSyntheticPopupNavigate("popup-2", "https://example.com/finish") | 66 | parentWV.handleSyntheticPopupNavigate("popup-2", "https://example.com/finish") | ||
| 67 | } | 67 | } | ||
| 68 | 68 | ||||
| 69 | + | func TestHandleSyntheticPopupOpen_CleansUpStateWhenPopupIsBlocked(t *testing.T) { | |||
| 70 | + | parentWV := &WebView{ | |||
| 71 | + | ctx: context.Background(), | |||
| 72 | + | id: port.WebViewID(28), | |||
| 73 | + | callbacks: &port.WebViewCallbacks{ | |||
| 74 | + | OnCreate: func(port.PopupRequest) port.WebView { | |||
| 75 | + | return nil | |||
| 76 | + | }, | |||
| 77 | + | }, | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | parentWV.handleSyntheticPopupNavigate("popup-blocked", "https://example.com/blocked") | |||
| 81 | + | parentWV.handleSyntheticPopupOpen("https://example.com/blocked", "_blank", "popup-blocked", true, false) | |||
| 82 | + | ||||
| 83 | + | require.Empty(t, parentWV.syntheticPopups) | |||
| 84 | + | } | |||
| 85 | + | ||||
| 69 | func TestHandleSyntheticPopupClose_ClosesMappedPopup(t *testing.T) { | 86 | func TestHandleSyntheticPopupClose_ClosesMappedPopup(t *testing.T) { | ||
| 70 | popupWV := &closeablePopupWebViewStub{MockWebView: portmocks.NewMockWebView(t)} | 87 | popupWV := &closeablePopupWebViewStub{MockWebView: portmocks.NewMockWebView(t)} | ||
| 71 | parentWV := &WebView{ | 88 | parentWV := &WebView{ | ||
M internal/ui/coordinator/content/coordinator.go +9 -7
| @@ -82,13 +82,15 @@ type Coordinator struct { | |||
| 82 | 82 | gestureActionHandler input.ActionHandler | |
| 83 | 83 | ||
| 84 | 84 | // Popup handling | |
| 85 | - | factory port.WebViewFactory | |
| 86 | - | popupConfig *entity.PopupBehaviorConfig | |
| 87 | - | pendingPopups map[port.WebViewID]*PendingPopup | |
| 88 | - | namedPopups map[namedPopupKey]*namedPopupState | |
| 89 | - | popupOAuth map[port.WebViewID]*popupOAuthState | |
| 90 | - | popupRefresh map[entity.PaneID]*time.Timer | |
| 91 | - | popupMu sync.RWMutex | |
| 85 | + | factory port.WebViewFactory | |
| 86 | + | popupConfig *entity.PopupBehaviorConfig | |
| 87 | + | pendingPopups map[port.WebViewID]*PendingPopup | |
| 88 | + | namedPopups map[namedPopupKey]*namedPopupState | |
| 89 | + | popupOAuth map[port.WebViewID]*popupOAuthState | |
| 90 | + | popupRefresh map[entity.PaneID]*time.Timer | |
| 91 | + | relatedPopupUnsupported bool | |
| 92 | + | relatedPopupSupportDetected bool | |
| 93 | + | popupMu sync.RWMutex | |
| 92 | 94 | ||
| 93 | 95 | // Callback to insert popup into workspace (avoids circular dependency) | |
| 94 | 96 | onInsertPopup func(ctx context.Context, input InsertPopupInput) error | |
| @@ -82,13 +82,15 @@ type Coordinator struct { | |||||
| 82 | gestureActionHandler input.ActionHandler | 82 | gestureActionHandler input.ActionHandler | ||
| 83 | 83 | ||||
| 84 | // Popup handling | 84 | // Popup handling | ||
| 85 | - | factory port.WebViewFactory | 85 | + | factory port.WebViewFactory |
| 86 | - | popupConfig *entity.PopupBehaviorConfig | 86 | + | popupConfig *entity.PopupBehaviorConfig |
| 87 | - | pendingPopups map[port.WebViewID]*PendingPopup | 87 | + | pendingPopups map[port.WebViewID]*PendingPopup |
| 88 | - | namedPopups map[namedPopupKey]*namedPopupState | 88 | + | namedPopups map[namedPopupKey]*namedPopupState |
| 89 | - | popupOAuth map[port.WebViewID]*popupOAuthState | 89 | + | popupOAuth map[port.WebViewID]*popupOAuthState |
| 90 | - | popupRefresh map[entity.PaneID]*time.Timer | 90 | + | popupRefresh map[entity.PaneID]*time.Timer |
| 91 | - | popupMu sync.RWMutex | 91 | + | relatedPopupUnsupported bool |
| 92 | + | relatedPopupSupportDetected bool | |||
| 93 | + | popupMu sync.RWMutex | |||
| 92 | 94 | ||||
| 93 | // Callback to insert popup into workspace (avoids circular dependency) | 95 | // Callback to insert popup into workspace (avoids circular dependency) | ||
| 94 | onInsertPopup func(ctx context.Context, input InsertPopupInput) error | 96 | onInsertPopup func(ctx context.Context, input InsertPopupInput) error | ||
M internal/ui/coordinator/content/popup.go +37 -2
| @@ -2,6 +2,7 @@ package content | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "errors" | |
| 5 | 6 | "fmt" | |
| 6 | 7 | neturl "net/url" | |
| 7 | 8 | "strings" | |
| @@ -218,6 +219,11 @@ func (c *Coordinator) SetPopupConfig( | |||
| 218 | 219 | c.factory = factory | |
| 219 | 220 | c.popupConfig = popupConfig | |
| 220 | 221 | c.generateID = generateID | |
| 222 | + | ||
| 223 | + | c.popupMu.Lock() | |
| 224 | + | c.relatedPopupUnsupported = false | |
| 225 | + | c.relatedPopupSupportDetected = false | |
| 226 | + | c.popupMu.Unlock() | |
| 221 | 227 | } | |
| 222 | 228 | ||
| 223 | 229 | // SetOnInsertPopup sets the callback to insert popups into the workspace. | |
| @@ -303,6 +309,26 @@ func normalizePopupTargetURIForFallback(rawTargetURI string) string { | |||
| 303 | 309 | return redirectParsed.String() | |
| 304 | 310 | } | |
| 305 | 311 | ||
| 312 | + | func (c *Coordinator) relatedPopupSupportDisabled() bool { | |
| 313 | + | c.popupMu.RLock() | |
| 314 | + | defer c.popupMu.RUnlock() | |
| 315 | + | return c.relatedPopupSupportDetected && c.relatedPopupUnsupported | |
| 316 | + | } | |
| 317 | + | ||
| 318 | + | func (c *Coordinator) markRelatedPopupUnsupported() { | |
| 319 | + | c.popupMu.Lock() | |
| 320 | + | defer c.popupMu.Unlock() | |
| 321 | + | c.relatedPopupSupportDetected = true | |
| 322 | + | c.relatedPopupUnsupported = true | |
| 323 | + | } | |
| 324 | + | ||
| 325 | + | func (c *Coordinator) markRelatedPopupSupported() { | |
| 326 | + | c.popupMu.Lock() | |
| 327 | + | defer c.popupMu.Unlock() | |
| 328 | + | c.relatedPopupSupportDetected = true | |
| 329 | + | c.relatedPopupUnsupported = false | |
| 330 | + | } | |
| 331 | + | ||
| 306 | 332 | // createPopupWebView prefers a related WebView so popups can share the | |
| 307 | 333 | // parent session/context. If the engine does not support related popup views, | |
| 308 | 334 | // it gracefully falls back to a regular WebView so target="_blank" and | |
| @@ -320,26 +346,35 @@ func (c *Coordinator) createPopupWebView( | |||
| 320 | 346 | log := logging.FromContext(ctx) | |
| 321 | 347 | relatedErr := error(nil) | |
| 322 | 348 | ||
| 323 | - | if !noJavaScriptAccess { | |
| 349 | + | if !noJavaScriptAccess && !c.relatedPopupSupportDisabled() { | |
| 324 | 350 | popupWV, err := c.factory.CreateRelated(ctx, parentID) | |
| 325 | 351 | if err == nil && popupWV != nil { | |
| 352 | + | c.markRelatedPopupSupported() | |
| 326 | 353 | return popupWV, targetURI, nil | |
| 327 | 354 | } | |
| 328 | 355 | if err == nil { | |
| 329 | 356 | err = fmt.Errorf("related popup webview factory returned nil without error") | |
| 330 | 357 | } | |
| 331 | 358 | relatedErr = err | |
| 359 | + | if errors.Is(err, port.ErrRelatedWebViewUnsupported) { | |
| 360 | + | c.markRelatedPopupUnsupported() | |
| 361 | + | } | |
| 332 | 362 | ||
| 333 | 363 | log.Debug(). | |
| 334 | 364 | Err(err). | |
| 335 | 365 | Uint64("parent_webview_id", uint64(parentID)). | |
| 336 | 366 | Str("target_uri", logging.TruncateURL(targetURI, logURLMaxLen)). | |
| 337 | 367 | Msg("related popup webview unavailable, falling back to regular webview") | |
| 338 | - | } else { | |
| 368 | + | } else if noJavaScriptAccess { | |
| 339 | 369 | log.Debug(). | |
| 340 | 370 | Uint64("parent_webview_id", uint64(parentID)). | |
| 341 | 371 | Str("target_uri", logging.TruncateURL(targetURI, logURLMaxLen)). | |
| 342 | 372 | Msg("popup requested no JavaScript opener access, creating regular webview") | |
| 373 | + | } else { | |
| 374 | + | log.Debug(). | |
| 375 | + | Uint64("parent_webview_id", uint64(parentID)). | |
| 376 | + | Str("target_uri", logging.TruncateURL(targetURI, logURLMaxLen)). | |
| 377 | + | Msg("related popup webviews known unsupported, creating regular webview") | |
| 343 | 378 | } | |
| 344 | 379 | ||
| 345 | 380 | popupWV, fallbackErr := c.factory.Create(ctx) | |
| @@ -2,6 +2,7 @@ package content | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "errors" | |||
| 5 | "fmt" | 6 | "fmt" | ||
| 6 | neturl "net/url" | 7 | neturl "net/url" | ||
| 7 | "strings" | 8 | "strings" | ||
| @@ -218,6 +219,11 @@ func (c *Coordinator) SetPopupConfig( | |||||
| 218 | c.factory = factory | 219 | c.factory = factory | ||
| 219 | c.popupConfig = popupConfig | 220 | c.popupConfig = popupConfig | ||
| 220 | c.generateID = generateID | 221 | c.generateID = generateID | ||
| 222 | + | ||||
| 223 | + | c.popupMu.Lock() | |||
| 224 | + | c.relatedPopupUnsupported = false | |||
| 225 | + | c.relatedPopupSupportDetected = false | |||
| 226 | + | c.popupMu.Unlock() | |||
| 221 | } | 227 | } | ||
| 222 | 228 | ||||
| 223 | // SetOnInsertPopup sets the callback to insert popups into the workspace. | 229 | // SetOnInsertPopup sets the callback to insert popups into the workspace. | ||
| @@ -303,6 +309,26 @@ func normalizePopupTargetURIForFallback(rawTargetURI string) string { | |||||
| 303 | return redirectParsed.String() | 309 | return redirectParsed.String() | ||
| 304 | } | 310 | } | ||
| 305 | 311 | ||||
| 312 | + | func (c *Coordinator) relatedPopupSupportDisabled() bool { | |||
| 313 | + | c.popupMu.RLock() | |||
| 314 | + | defer c.popupMu.RUnlock() | |||
| 315 | + | return c.relatedPopupSupportDetected && c.relatedPopupUnsupported | |||
| 316 | + | } | |||
| 317 | + | ||||
| 318 | + | func (c *Coordinator) markRelatedPopupUnsupported() { | |||
| 319 | + | c.popupMu.Lock() | |||
| 320 | + | defer c.popupMu.Unlock() | |||
| 321 | + | c.relatedPopupSupportDetected = true | |||
| 322 | + | c.relatedPopupUnsupported = true | |||
| 323 | + | } | |||
| 324 | + | ||||
| 325 | + | func (c *Coordinator) markRelatedPopupSupported() { | |||
| 326 | + | c.popupMu.Lock() | |||
| 327 | + | defer c.popupMu.Unlock() | |||
| 328 | + | c.relatedPopupSupportDetected = true | |||
| 329 | + | c.relatedPopupUnsupported = false | |||
| 330 | + | } | |||
| 331 | + | ||||
| 306 | // createPopupWebView prefers a related WebView so popups can share the | 332 | // createPopupWebView prefers a related WebView so popups can share the | ||
| 307 | // parent session/context. If the engine does not support related popup views, | 333 | // parent session/context. If the engine does not support related popup views, | ||
| 308 | // it gracefully falls back to a regular WebView so target="_blank" and | 334 | // it gracefully falls back to a regular WebView so target="_blank" and | ||
| @@ -320,26 +346,35 @@ func (c *Coordinator) createPopupWebView( | |||||
| 320 | log := logging.FromContext(ctx) | 346 | log := logging.FromContext(ctx) | ||
| 321 | relatedErr := error(nil) | 347 | relatedErr := error(nil) | ||
| 322 | 348 | ||||
| 323 | - | if !noJavaScriptAccess { | 349 | + | if !noJavaScriptAccess && !c.relatedPopupSupportDisabled() { |
| 324 | popupWV, err := c.factory.CreateRelated(ctx, parentID) | 350 | popupWV, err := c.factory.CreateRelated(ctx, parentID) | ||
| 325 | if err == nil && popupWV != nil { | 351 | if err == nil && popupWV != nil { | ||
| 352 | + | c.markRelatedPopupSupported() | |||
| 326 | return popupWV, targetURI, nil | 353 | return popupWV, targetURI, nil | ||
| 327 | } | 354 | } | ||
| 328 | if err == nil { | 355 | if err == nil { | ||
| 329 | err = fmt.Errorf("related popup webview factory returned nil without error") | 356 | err = fmt.Errorf("related popup webview factory returned nil without error") | ||
| 330 | } | 357 | } | ||
| 331 | relatedErr = err | 358 | relatedErr = err | ||
| 359 | + | if errors.Is(err, port.ErrRelatedWebViewUnsupported) { | |||
| 360 | + | c.markRelatedPopupUnsupported() | |||
| 361 | + | } | |||
| 332 | 362 | ||||
| 333 | log.Debug(). | 363 | log.Debug(). | ||
| 334 | Err(err). | 364 | Err(err). | ||
| 335 | Uint64("parent_webview_id", uint64(parentID)). | 365 | Uint64("parent_webview_id", uint64(parentID)). | ||
| 336 | Str("target_uri", logging.TruncateURL(targetURI, logURLMaxLen)). | 366 | Str("target_uri", logging.TruncateURL(targetURI, logURLMaxLen)). | ||
| 337 | Msg("related popup webview unavailable, falling back to regular webview") | 367 | Msg("related popup webview unavailable, falling back to regular webview") | ||
| 338 | - | } else { | 368 | + | } else if noJavaScriptAccess { |
| 339 | log.Debug(). | 369 | log.Debug(). | ||
| 340 | Uint64("parent_webview_id", uint64(parentID)). | 370 | Uint64("parent_webview_id", uint64(parentID)). | ||
| 341 | Str("target_uri", logging.TruncateURL(targetURI, logURLMaxLen)). | 371 | Str("target_uri", logging.TruncateURL(targetURI, logURLMaxLen)). | ||
| 342 | Msg("popup requested no JavaScript opener access, creating regular webview") | 372 | Msg("popup requested no JavaScript opener access, creating regular webview") | ||
| 373 | + | } else { | |||
| 374 | + | log.Debug(). | |||
| 375 | + | Uint64("parent_webview_id", uint64(parentID)). | |||
| 376 | + | Str("target_uri", logging.TruncateURL(targetURI, logURLMaxLen)). | |||
| 377 | + | Msg("related popup webviews known unsupported, creating regular webview") | |||
| 343 | } | 378 | } | ||
| 344 | 379 | ||||
| 345 | popupWV, fallbackErr := c.factory.Create(ctx) | 380 | popupWV, fallbackErr := c.factory.Create(ctx) | ||
M internal/ui/coordinator/content/popup_test.go +52 -0
| @@ -181,6 +181,58 @@ func TestHandlePopupCreate_UsesRegularWebViewWhenPopupDisablesJavaScriptAccess(t | |||
| 181 | 181 | require.Same(t, popupWV, created) | |
| 182 | 182 | } | |
| 183 | 183 | ||
| 184 | + | func TestHandlePopupCreate_SkipsRelatedCreateAfterUnsupportedFactoryError(t *testing.T) { | |
| 185 | + | ctx := context.Background() | |
| 186 | + | parentPaneID := entity.PaneID("parent-pane") | |
| 187 | + | parentWV := mocks.NewMockWebView(t) | |
| 188 | + | parentWV.EXPECT().ID().Return(port.WebViewID(101)).Twice() | |
| 189 | + | ||
| 190 | + | firstPopupWV := mocks.NewMockWebView(t) | |
| 191 | + | firstPopupWV.EXPECT().ID().Return(port.WebViewID(202)).Once() | |
| 192 | + | firstPopupWV.EXPECT().Generation().Return(uint64(1)).Once() | |
| 193 | + | firstPopupWV.EXPECT().SetCallbacks(mock.Anything).Once() | |
| 194 | + | firstPopupWV.EXPECT().IsLoading().Return(false).Once() | |
| 195 | + | firstPopupWV.EXPECT().URI().Return("").Once() | |
| 196 | + | firstPopupWV.EXPECT().LoadURI(mock.Anything, "https://example.com/first").Return(nil).Once() | |
| 197 | + | ||
| 198 | + | secondPopupWV := mocks.NewMockWebView(t) | |
| 199 | + | secondPopupWV.EXPECT().ID().Return(port.WebViewID(203)).Once() | |
| 200 | + | secondPopupWV.EXPECT().Generation().Return(uint64(1)).Once() | |
| 201 | + | secondPopupWV.EXPECT().SetCallbacks(mock.Anything).Once() | |
| 202 | + | secondPopupWV.EXPECT().IsLoading().Return(false).Once() | |
| 203 | + | secondPopupWV.EXPECT().URI().Return("").Once() | |
| 204 | + | secondPopupWV.EXPECT().LoadURI(mock.Anything, "https://example.com/second").Return(nil).Once() | |
| 205 | + | ||
| 206 | + | factory := mocks.NewMockWebViewFactory(t) | |
| 207 | + | factory.EXPECT().CreateRelated(mock.Anything, port.WebViewID(101)).Return(nil, port.ErrRelatedWebViewUnsupported).Once() | |
| 208 | + | factory.EXPECT().Create(mock.Anything).Return(firstPopupWV, nil).Once() | |
| 209 | + | factory.EXPECT().Create(mock.Anything).Return(secondPopupWV, nil).Once() | |
| 210 | + | ||
| 211 | + | c := &Coordinator{ | |
| 212 | + | webViews: make(map[entity.PaneID]port.WebView), | |
| 213 | + | pendingPopups: make(map[port.WebViewID]*PendingPopup), | |
| 214 | + | popupOAuth: make(map[port.WebViewID]*popupOAuthState), | |
| 215 | + | } | |
| 216 | + | c.SetPopupConfig(factory, nil, nil) | |
| 217 | + | c.SetOnInsertPopup(func(_ context.Context, input InsertPopupInput) error { | |
| 218 | + | return nil | |
| 219 | + | }) | |
| 220 | + | ||
| 221 | + | first := c.handlePopupCreate(ctx, parentPaneID, parentWV, port.PopupRequest{ | |
| 222 | + | TargetURI: "https://example.com/first", | |
| 223 | + | FrameName: "_blank", | |
| 224 | + | IsUserGesture: true, | |
| 225 | + | }) | |
| 226 | + | second := c.handlePopupCreate(ctx, parentPaneID, parentWV, port.PopupRequest{ | |
| 227 | + | TargetURI: "https://example.com/second", | |
| 228 | + | FrameName: "_blank", | |
| 229 | + | IsUserGesture: true, | |
| 230 | + | }) | |
| 231 | + | ||
| 232 | + | require.Same(t, firstPopupWV, first) | |
| 233 | + | require.Same(t, secondPopupWV, second) | |
| 234 | + | } | |
| 235 | + | ||
| 184 | 236 | func TestHandlePopupCreate_FallsBackToRegularWebViewWhenRelatedCreateFails(t *testing.T) { | |
| 185 | 237 | ctx := context.Background() | |
| 186 | 238 | parentPaneID := entity.PaneID("parent-pane") | |
| @@ -181,6 +181,58 @@ func TestHandlePopupCreate_UsesRegularWebViewWhenPopupDisablesJavaScriptAccess(t | |||||
| 181 | require.Same(t, popupWV, created) | 181 | require.Same(t, popupWV, created) | ||
| 182 | } | 182 | } | ||
| 183 | 183 | ||||
| 184 | + | func TestHandlePopupCreate_SkipsRelatedCreateAfterUnsupportedFactoryError(t *testing.T) { | |||
| 185 | + | ctx := context.Background() | |||
| 186 | + | parentPaneID := entity.PaneID("parent-pane") | |||
| 187 | + | parentWV := mocks.NewMockWebView(t) | |||
| 188 | + | parentWV.EXPECT().ID().Return(port.WebViewID(101)).Twice() | |||
| 189 | + | ||||
| 190 | + | firstPopupWV := mocks.NewMockWebView(t) | |||
| 191 | + | firstPopupWV.EXPECT().ID().Return(port.WebViewID(202)).Once() | |||
| 192 | + | firstPopupWV.EXPECT().Generation().Return(uint64(1)).Once() | |||
| 193 | + | firstPopupWV.EXPECT().SetCallbacks(mock.Anything).Once() | |||
| 194 | + | firstPopupWV.EXPECT().IsLoading().Return(false).Once() | |||
| 195 | + | firstPopupWV.EXPECT().URI().Return("").Once() | |||
| 196 | + | firstPopupWV.EXPECT().LoadURI(mock.Anything, "https://example.com/first").Return(nil).Once() | |||
| 197 | + | ||||
| 198 | + | secondPopupWV := mocks.NewMockWebView(t) | |||
| 199 | + | secondPopupWV.EXPECT().ID().Return(port.WebViewID(203)).Once() | |||
| 200 | + | secondPopupWV.EXPECT().Generation().Return(uint64(1)).Once() | |||
| 201 | + | secondPopupWV.EXPECT().SetCallbacks(mock.Anything).Once() | |||
| 202 | + | secondPopupWV.EXPECT().IsLoading().Return(false).Once() | |||
| 203 | + | secondPopupWV.EXPECT().URI().Return("").Once() | |||
| 204 | + | secondPopupWV.EXPECT().LoadURI(mock.Anything, "https://example.com/second").Return(nil).Once() | |||
| 205 | + | ||||
| 206 | + | factory := mocks.NewMockWebViewFactory(t) | |||
| 207 | + | factory.EXPECT().CreateRelated(mock.Anything, port.WebViewID(101)).Return(nil, port.ErrRelatedWebViewUnsupported).Once() | |||
| 208 | + | factory.EXPECT().Create(mock.Anything).Return(firstPopupWV, nil).Once() | |||
| 209 | + | factory.EXPECT().Create(mock.Anything).Return(secondPopupWV, nil).Once() | |||
| 210 | + | ||||
| 211 | + | c := &Coordinator{ | |||
| 212 | + | webViews: make(map[entity.PaneID]port.WebView), | |||
| 213 | + | pendingPopups: make(map[port.WebViewID]*PendingPopup), | |||
| 214 | + | popupOAuth: make(map[port.WebViewID]*popupOAuthState), | |||
| 215 | + | } | |||
| 216 | + | c.SetPopupConfig(factory, nil, nil) | |||
| 217 | + | c.SetOnInsertPopup(func(_ context.Context, input InsertPopupInput) error { | |||
| 218 | + | return nil | |||
| 219 | + | }) | |||
| 220 | + | ||||
| 221 | + | first := c.handlePopupCreate(ctx, parentPaneID, parentWV, port.PopupRequest{ | |||
| 222 | + | TargetURI: "https://example.com/first", | |||
| 223 | + | FrameName: "_blank", | |||
| 224 | + | IsUserGesture: true, | |||
| 225 | + | }) | |||
| 226 | + | second := c.handlePopupCreate(ctx, parentPaneID, parentWV, port.PopupRequest{ | |||
| 227 | + | TargetURI: "https://example.com/second", | |||
| 228 | + | FrameName: "_blank", | |||
| 229 | + | IsUserGesture: true, | |||
| 230 | + | }) | |||
| 231 | + | ||||
| 232 | + | require.Same(t, firstPopupWV, first) | |||
| 233 | + | require.Same(t, secondPopupWV, second) | |||
| 234 | + | } | |||
| 235 | + | ||||
| 184 | func TestHandlePopupCreate_FallsBackToRegularWebViewWhenRelatedCreateFails(t *testing.T) { | 236 | func TestHandlePopupCreate_FallsBackToRegularWebViewWhenRelatedCreateFails(t *testing.T) { | ||
| 185 | ctx := context.Background() | 237 | ctx := context.Background() | ||
| 186 | parentPaneID := entity.PaneID("parent-pane") | 238 | parentPaneID := entity.PaneID("parent-pane") | ||