cef: install popup opener bridge in renderer
- commit
- 71feac4d83efe4a0d185dfe46ce9b7e231f7fbf1
- author
- brice · · 2026-04-23 04:32 UTC
- parent
- 1df2f44
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +465 -11
M internal/infrastructure/cef/app.go +9 -7
| @@ -45,6 +45,7 @@ func configureCommandLine(commandLine purecef.CommandLine) { | |||
| 45 | 45 | type dumberApp struct { | |
| 46 | 46 | engine *Engine | |
| 47 | 47 | bph purecef.BrowserProcessHandler | |
| 48 | + | rph purecef.RenderProcessHandler | |
| 48 | 49 | } | |
| 49 | 50 | ||
| 50 | 51 | // newDumberApp creates an App whose BrowserProcessHandler handles context | |
| @@ -53,15 +54,16 @@ type dumberApp struct { | |||
| 53 | 54 | func newDumberApp(engine *Engine) purecef.App { | |
| 54 | 55 | app := &dumberApp{engine: engine} | |
| 55 | 56 | app.bph = &dumberBPH{engine: engine} | |
| 57 | + | app.rph = newPopupOpenerRenderProcessHandler() | |
| 56 | 58 | return app | |
| 57 | 59 | } | |
| 58 | 60 | ||
| 59 | 61 | // NewSubprocessApp returns a lightweight raw App implementation for CEF | |
| 60 | - | // subprocesses re-executed from the main dumber binary. Keep subprocesses free | |
| 61 | - | // of custom render handlers for now: the renderer bridge introduced a startup | |
| 62 | - | // regression where OSR pages stopped producing their first paint. | |
| 62 | + | // subprocesses re-executed from the main dumber binary. Keep the legacy | |
| 63 | + | // renderer bridge disabled, but allow the minimal popup-opener render handler | |
| 64 | + | // needed to install synthetic opener state before page scripts run. | |
| 63 | 65 | func NewSubprocessApp() purecef.App { | |
| 64 | - | return &subprocessApp{} | |
| 66 | + | return &subprocessApp{rph: newPopupOpenerRenderProcessHandler()} | |
| 65 | 67 | } | |
| 66 | 68 | ||
| 67 | 69 | // maxCmdLineLogLen limits logged command line length to avoid leaking sensitive paths. | |
| @@ -91,9 +93,9 @@ func (a *dumberApp) OnRegisterCustomSchemes(registrar purecef.SchemeRegistrar) { | |||
| 91 | 93 | } | |
| 92 | 94 | func (a *dumberApp) GetResourceBundleHandler() purecef.ResourceBundleHandler { return nil } | |
| 93 | 95 | func (a *dumberApp) GetBrowserProcessHandler() purecef.BrowserProcessHandler { return a.bph } | |
| 94 | - | func (a *dumberApp) GetRenderProcessHandler() purecef.RenderProcessHandler { return nil } | |
| 96 | + | func (a *dumberApp) GetRenderProcessHandler() purecef.RenderProcessHandler { return a.rph } | |
| 95 | 97 | ||
| 96 | - | type subprocessApp struct{} | |
| 98 | + | type subprocessApp struct{ rph purecef.RenderProcessHandler } | |
| 97 | 99 | ||
| 98 | 100 | func (a *subprocessApp) OnBeforeCommandLineProcessing(_ string, commandLine purecef.CommandLine) { | |
| 99 | 101 | configureCommandLine(commandLine) | |
| @@ -103,7 +105,7 @@ func (a *subprocessApp) OnRegisterCustomSchemes(registrar purecef.SchemeRegistra | |||
| 103 | 105 | } | |
| 104 | 106 | func (a *subprocessApp) GetResourceBundleHandler() purecef.ResourceBundleHandler { return nil } | |
| 105 | 107 | func (a *subprocessApp) GetBrowserProcessHandler() purecef.BrowserProcessHandler { return nil } | |
| 106 | - | func (a *subprocessApp) GetRenderProcessHandler() purecef.RenderProcessHandler { return nil } | |
| 108 | + | func (a *subprocessApp) GetRenderProcessHandler() purecef.RenderProcessHandler { return a.rph } | |
| 107 | 109 | ||
| 108 | 110 | // dumberBPH implements purecef.BrowserProcessHandler for context initialization, | |
| 109 | 111 | // child process launch tracking, and diagnostic logging. | |
| @@ -45,6 +45,7 @@ func configureCommandLine(commandLine purecef.CommandLine) { | |||||
| 45 | type dumberApp struct { | 45 | type dumberApp struct { | ||
| 46 | engine *Engine | 46 | engine *Engine | ||
| 47 | bph purecef.BrowserProcessHandler | 47 | bph purecef.BrowserProcessHandler | ||
| 48 | + | rph purecef.RenderProcessHandler | |||
| 48 | } | 49 | } | ||
| 49 | 50 | ||||
| 50 | // newDumberApp creates an App whose BrowserProcessHandler handles context | 51 | // newDumberApp creates an App whose BrowserProcessHandler handles context | ||
| @@ -53,15 +54,16 @@ type dumberApp struct { | |||||
| 53 | func newDumberApp(engine *Engine) purecef.App { | 54 | func newDumberApp(engine *Engine) purecef.App { | ||
| 54 | app := &dumberApp{engine: engine} | 55 | app := &dumberApp{engine: engine} | ||
| 55 | app.bph = &dumberBPH{engine: engine} | 56 | app.bph = &dumberBPH{engine: engine} | ||
| 57 | + | app.rph = newPopupOpenerRenderProcessHandler() | |||
| 56 | return app | 58 | return app | ||
| 57 | } | 59 | } | ||
| 58 | 60 | ||||
| 59 | // NewSubprocessApp returns a lightweight raw App implementation for CEF | 61 | // NewSubprocessApp returns a lightweight raw App implementation for CEF | ||
| 60 | - | // subprocesses re-executed from the main dumber binary. Keep subprocesses free | 62 | + | // subprocesses re-executed from the main dumber binary. Keep the legacy |
| 61 | - | // of custom render handlers for now: the renderer bridge introduced a startup | 63 | + | // renderer bridge disabled, but allow the minimal popup-opener render handler |
| 62 | - | // regression where OSR pages stopped producing their first paint. | 64 | + | // needed to install synthetic opener state before page scripts run. |
| 63 | func NewSubprocessApp() purecef.App { | 65 | func NewSubprocessApp() purecef.App { | ||
| 64 | - | return &subprocessApp{} | 66 | + | return &subprocessApp{rph: newPopupOpenerRenderProcessHandler()} |
| 65 | } | 67 | } | ||
| 66 | 68 | ||||
| 67 | // maxCmdLineLogLen limits logged command line length to avoid leaking sensitive paths. | 69 | // maxCmdLineLogLen limits logged command line length to avoid leaking sensitive paths. | ||
| @@ -91,9 +93,9 @@ func (a *dumberApp) OnRegisterCustomSchemes(registrar purecef.SchemeRegistrar) { | |||||
| 91 | } | 93 | } | ||
| 92 | func (a *dumberApp) GetResourceBundleHandler() purecef.ResourceBundleHandler { return nil } | 94 | func (a *dumberApp) GetResourceBundleHandler() purecef.ResourceBundleHandler { return nil } | ||
| 93 | func (a *dumberApp) GetBrowserProcessHandler() purecef.BrowserProcessHandler { return a.bph } | 95 | func (a *dumberApp) GetBrowserProcessHandler() purecef.BrowserProcessHandler { return a.bph } | ||
| 94 | - | func (a *dumberApp) GetRenderProcessHandler() purecef.RenderProcessHandler { return nil } | 96 | + | func (a *dumberApp) GetRenderProcessHandler() purecef.RenderProcessHandler { return a.rph } |
| 95 | 97 | ||||
| 96 | - | type subprocessApp struct{} | 98 | + | type subprocessApp struct{ rph purecef.RenderProcessHandler } |
| 97 | 99 | ||||
| 98 | func (a *subprocessApp) OnBeforeCommandLineProcessing(_ string, commandLine purecef.CommandLine) { | 100 | func (a *subprocessApp) OnBeforeCommandLineProcessing(_ string, commandLine purecef.CommandLine) { | ||
| 99 | configureCommandLine(commandLine) | 101 | configureCommandLine(commandLine) | ||
| @@ -103,7 +105,7 @@ func (a *subprocessApp) OnRegisterCustomSchemes(registrar purecef.SchemeRegistra | |||||
| 103 | } | 105 | } | ||
| 104 | func (a *subprocessApp) GetResourceBundleHandler() purecef.ResourceBundleHandler { return nil } | 106 | func (a *subprocessApp) GetResourceBundleHandler() purecef.ResourceBundleHandler { return nil } | ||
| 105 | func (a *subprocessApp) GetBrowserProcessHandler() purecef.BrowserProcessHandler { return nil } | 107 | func (a *subprocessApp) GetBrowserProcessHandler() purecef.BrowserProcessHandler { return nil } | ||
| 106 | - | func (a *subprocessApp) GetRenderProcessHandler() purecef.RenderProcessHandler { return nil } | 108 | + | func (a *subprocessApp) GetRenderProcessHandler() purecef.RenderProcessHandler { return a.rph } |
| 107 | 109 | ||||
| 108 | // dumberBPH implements purecef.BrowserProcessHandler for context initialization, | 110 | // dumberBPH implements purecef.BrowserProcessHandler for context initialization, | ||
| 109 | // child process launch tracking, and diagnostic logging. | 111 | // child process launch tracking, and diagnostic logging. | ||
M internal/infrastructure/cef/factory.go +1 -1
| @@ -247,7 +247,7 @@ func (f *WebViewFactory) postPendingBrowserCreate(ctx context.Context, wv *WebVi | |||
| 247 | 247 | pc.client, | |
| 248 | 248 | initialURL, | |
| 249 | 249 | pc.settings, | |
| 250 | - | nil, // extraInfo | |
| 250 | + | pc.extraInfo, | |
| 251 | 251 | nil, // requestContext | |
| 252 | 252 | ) | |
| 253 | 253 | log.Debug(). | |
| @@ -247,7 +247,7 @@ func (f *WebViewFactory) postPendingBrowserCreate(ctx context.Context, wv *WebVi | |||||
| 247 | pc.client, | 247 | pc.client, | ||
| 248 | initialURL, | 248 | initialURL, | ||
| 249 | pc.settings, | 249 | pc.settings, | ||
| 250 | - | nil, // extraInfo | 250 | + | pc.extraInfo, |
| 251 | nil, // requestContext | 251 | nil, // requestContext | ||
| 252 | ) | 252 | ) | ||
| 253 | log.Debug(). | 253 | log.Debug(). | ||
M internal/infrastructure/cef/popup_opener_bridge.go +120 -1
| @@ -100,6 +100,49 @@ func (wv *WebView) HasActivePopupOpenerBridge() bool { | |||
| 100 | 100 | return !wv.popupNoJavaScriptAccess && wv.popupOpenerBridgeParent != nil | |
| 101 | 101 | } | |
| 102 | 102 | ||
| 103 | + | func (wv *WebView) popupOpenerBridgeState() (parentURI string, active bool, blocked bool) { | |
| 104 | + | if wv == nil { | |
| 105 | + | return "", false, false | |
| 106 | + | } | |
| 107 | + | wv.mu.RLock() | |
| 108 | + | defer wv.mu.RUnlock() | |
| 109 | + | blocked = wv.popupNoJavaScriptAccess | |
| 110 | + | active = !blocked && wv.popupOpenerBridgeParent != nil | |
| 111 | + | parentURI = wv.popupOpenerBridgeParentURI | |
| 112 | + | return parentURI, active, blocked | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | func (wv *WebView) ensureBridgeNonceLocked() string { | |
| 116 | + | if wv == nil { | |
| 117 | + | return "" | |
| 118 | + | } | |
| 119 | + | if wv.bridgeNonce != "" { | |
| 120 | + | return wv.bridgeNonce | |
| 121 | + | } | |
| 122 | + | nonce := newBridgeNonce() | |
| 123 | + | if nonce == "" { | |
| 124 | + | return "" | |
| 125 | + | } | |
| 126 | + | wv.bridgeNonce = nonce | |
| 127 | + | return nonce | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | func (wv *WebView) syncPopupOpenerBridgeExtraInfoLocked() { | |
| 131 | + | if wv == nil || wv.pendingCreate == nil || wv.pendingCreate.windowInfo == nil { | |
| 132 | + | return | |
| 133 | + | } | |
| 134 | + | if wv.popupNoJavaScriptAccess || wv.popupOpenerBridgeParent == nil || wv.popupOpenerBridgeParentURI == "" { | |
| 135 | + | wv.pendingCreate.extraInfo = nil | |
| 136 | + | return | |
| 137 | + | } | |
| 138 | + | bridgeNonce := wv.ensureBridgeNonceLocked() | |
| 139 | + | if bridgeNonce == "" { | |
| 140 | + | wv.pendingCreate.extraInfo = nil | |
| 141 | + | return | |
| 142 | + | } | |
| 143 | + | wv.pendingCreate.extraInfo = popupOpenerRenderExtraInfoBuilder(wv.popupOpenerBridgeParentURI, bridgeNonce) | |
| 144 | + | } | |
| 145 | + | ||
| 103 | 146 | // EnablePopupOpenerBridge implements port.PopupOpenerCapable. | |
| 104 | 147 | func (wv *WebView) EnablePopupOpenerBridge(parent port.WebView, noJavaScriptAccess bool) { | |
| 105 | 148 | if wv == nil { | |
| @@ -111,16 +154,37 @@ func (wv *WebView) EnablePopupOpenerBridge(parent port.WebView, noJavaScriptAcce | |||
| 111 | 154 | if noJavaScriptAccess { | |
| 112 | 155 | wv.popupOpenerBridgeParent = nil | |
| 113 | 156 | wv.popupOpenerBridgeParentURI = "" | |
| 157 | + | wv.syncPopupOpenerBridgeExtraInfoLocked() | |
| 158 | + | if wv.ctx != nil { | |
| 159 | + | logging.FromContext(wv.ctx).Debug(). | |
| 160 | + | Uint64("webview_id", uint64(wv.id)). | |
| 161 | + | Bool("no_javascript_access", true). | |
| 162 | + | Msg("cef: popup opener bridge disabled") | |
| 163 | + | } | |
| 114 | 164 | return | |
| 115 | 165 | } | |
| 116 | 166 | parentWV, ok := parent.(*WebView) | |
| 117 | 167 | if !ok || parentWV == nil { | |
| 118 | 168 | wv.popupOpenerBridgeParent = nil | |
| 119 | 169 | wv.popupOpenerBridgeParentURI = "" | |
| 170 | + | wv.syncPopupOpenerBridgeExtraInfoLocked() | |
| 171 | + | if wv.ctx != nil { | |
| 172 | + | logging.FromContext(wv.ctx).Debug(). | |
| 173 | + | Uint64("webview_id", uint64(wv.id)). | |
| 174 | + | Msg("cef: popup opener bridge unavailable: parent missing") | |
| 175 | + | } | |
| 120 | 176 | return | |
| 121 | 177 | } | |
| 122 | 178 | wv.popupOpenerBridgeParent = parentWV | |
| 123 | 179 | wv.popupOpenerBridgeParentURI = parentWV.URI() | |
| 180 | + | wv.syncPopupOpenerBridgeExtraInfoLocked() | |
| 181 | + | if wv.ctx != nil { | |
| 182 | + | logging.FromContext(wv.ctx).Debug(). | |
| 183 | + | Uint64("webview_id", uint64(wv.id)). | |
| 184 | + | Uint64("parent_webview_id", uint64(parentWV.id)). | |
| 185 | + | Str("parent_uri", logging.TruncateURL(wv.popupOpenerBridgeParentURI, logging.PermissionLogURLMaxLen)). | |
| 186 | + | Msg("cef: popup opener bridge enabled") | |
| 187 | + | } | |
| 124 | 188 | } | |
| 125 | 189 | ||
| 126 | 190 | func (wv *WebView) popupOpenerBridgeScript(bridgeNonce string) string { | |
| @@ -131,11 +195,26 @@ func (wv *WebView) popupOpenerBridgeScript(bridgeNonce string) string { | |||
| 131 | 195 | wv.mu.RLock() | |
| 132 | 196 | parentURI := wv.popupOpenerBridgeParentURI | |
| 133 | 197 | parent := wv.popupOpenerBridgeParent | |
| 134 | - | blocked := wv.popupNoJavaScriptAccess || parent == nil | |
| 198 | + | noJavaScriptAccess := wv.popupNoJavaScriptAccess | |
| 199 | + | blocked := noJavaScriptAccess || parent == nil | |
| 135 | 200 | wv.mu.RUnlock() | |
| 136 | 201 | if blocked { | |
| 202 | + | if wv.ctx != nil { | |
| 203 | + | logging.FromContext(wv.ctx).Debug(). | |
| 204 | + | Uint64("webview_id", uint64(wv.id)). | |
| 205 | + | Bool("blocked", true). | |
| 206 | + | Bool("has_parent", parent != nil). | |
| 207 | + | Bool("no_javascript_access", noJavaScriptAccess). | |
| 208 | + | Msg("cef: popup opener bridge script skipped") | |
| 209 | + | } | |
| 137 | 210 | return "" | |
| 138 | 211 | } | |
| 212 | + | if wv.ctx != nil { | |
| 213 | + | logging.FromContext(wv.ctx).Debug(). | |
| 214 | + | Uint64("webview_id", uint64(wv.id)). | |
| 215 | + | Str("parent_uri", logging.TruncateURL(parentURI, logging.PermissionLogURLMaxLen)). | |
| 216 | + | Msg("cef: popup opener bridge script prepared") | |
| 217 | + | } | |
| 139 | 218 | ||
| 140 | 219 | return buildPopupOpenerBridgeJS(bridgeNonce, parentURI) | |
| 141 | 220 | } | |
| @@ -154,13 +233,32 @@ func (wv *WebView) handlePopupOpenerNavigate(targetURL string) { | |||
| 154 | 233 | opener := wv.popupOpenerBridgeParent | |
| 155 | 234 | wv.mu.RUnlock() | |
| 156 | 235 | if opener == nil || opener.destroyed.Load() { | |
| 236 | + | if wv.ctx != nil { | |
| 237 | + | logging.FromContext(wv.ctx).Debug(). | |
| 238 | + | Uint64("webview_id", uint64(wv.id)). | |
| 239 | + | Str("target_url", logging.TruncateURL(trimmedURL, logging.PermissionLogURLMaxLen)). | |
| 240 | + | Msg("cef: popup opener navigate ignored: opener unavailable") | |
| 241 | + | } | |
| 157 | 242 | return | |
| 158 | 243 | } | |
| 159 | 244 | ||
| 160 | 245 | resolvedURL := resolvePopupOpenerNavigationTarget(trimmedURL, opener.URI()) | |
| 161 | 246 | if resolvedURL == "" { | |
| 247 | + | if wv.ctx != nil { | |
| 248 | + | logging.FromContext(wv.ctx).Warn(). | |
| 249 | + | Uint64("webview_id", uint64(wv.id)). | |
| 250 | + | Str("target_url", logging.TruncateURL(trimmedURL, logging.PermissionLogURLMaxLen)). | |
| 251 | + | Msg("cef: popup opener navigate rejected") | |
| 252 | + | } | |
| 162 | 253 | return | |
| 163 | 254 | } | |
| 255 | + | if wv.ctx != nil { | |
| 256 | + | logging.FromContext(wv.ctx).Debug(). | |
| 257 | + | Uint64("webview_id", uint64(wv.id)). | |
| 258 | + | Str("target_url", logging.TruncateURL(trimmedURL, logging.PermissionLogURLMaxLen)). | |
| 259 | + | Str("resolved_url", logging.TruncateURL(resolvedURL, logging.PermissionLogURLMaxLen)). | |
| 260 | + | Msg("cef: popup opener navigate received") | |
| 261 | + | } | |
| 164 | 262 | ||
| 165 | 263 | if err := opener.LoadURI(context.Background(), resolvedURL); err != nil && opener.ctx != nil { | |
| 166 | 264 | logging.FromContext(opener.ctx).Warn(). | |
| @@ -224,11 +322,32 @@ func (wv *WebView) handlePopupOpenerPostMessage(payload popupOpenerPostMessagePa | |||
| 224 | 322 | opener := wv.popupOpenerBridgeParent | |
| 225 | 323 | wv.mu.RUnlock() | |
| 226 | 324 | if opener == nil || opener.destroyed.Load() { | |
| 325 | + | if wv.ctx != nil { | |
| 326 | + | logging.FromContext(wv.ctx).Debug(). | |
| 327 | + | Uint64("webview_id", uint64(wv.id)). | |
| 328 | + | Str("target_origin", payload.TargetOrigin). | |
| 329 | + | Msg("cef: popup opener postMessage ignored: opener unavailable") | |
| 330 | + | } | |
| 227 | 331 | return | |
| 228 | 332 | } | |
| 229 | 333 | if !targetOriginMatchesPopupOpener(payload.TargetOrigin, opener.URI()) { | |
| 334 | + | if wv.ctx != nil { | |
| 335 | + | logging.FromContext(wv.ctx).Warn(). | |
| 336 | + | Uint64("webview_id", uint64(wv.id)). | |
| 337 | + | Str("target_origin", payload.TargetOrigin). | |
| 338 | + | Str("opener_uri", logging.TruncateURL(opener.URI(), logging.PermissionLogURLMaxLen)). | |
| 339 | + | Msg("cef: popup opener postMessage rejected") | |
| 340 | + | } | |
| 230 | 341 | return | |
| 231 | 342 | } | |
| 343 | + | if wv.ctx != nil { | |
| 344 | + | logging.FromContext(wv.ctx).Debug(). | |
| 345 | + | Uint64("webview_id", uint64(wv.id)). | |
| 346 | + | Str("target_origin", payload.TargetOrigin). | |
| 347 | + | Str("source_origin", payload.SourceOrigin). | |
| 348 | + | Str("source_href", logging.TruncateURL(payload.SourceHref, logging.PermissionLogURLMaxLen)). | |
| 349 | + | Msg("cef: popup opener postMessage received") | |
| 350 | + | } | |
| 232 | 351 | ||
| 233 | 352 | sourceOrigin := popupSourceOrigin(payload) | |
| 234 | 353 | dataExpr := "undefined" | |
| @@ -100,6 +100,49 @@ func (wv *WebView) HasActivePopupOpenerBridge() bool { | |||||
| 100 | return !wv.popupNoJavaScriptAccess && wv.popupOpenerBridgeParent != nil | 100 | return !wv.popupNoJavaScriptAccess && wv.popupOpenerBridgeParent != nil | ||
| 101 | } | 101 | } | ||
| 102 | 102 | ||||
| 103 | + | func (wv *WebView) popupOpenerBridgeState() (parentURI string, active bool, blocked bool) { | |||
| 104 | + | if wv == nil { | |||
| 105 | + | return "", false, false | |||
| 106 | + | } | |||
| 107 | + | wv.mu.RLock() | |||
| 108 | + | defer wv.mu.RUnlock() | |||
| 109 | + | blocked = wv.popupNoJavaScriptAccess | |||
| 110 | + | active = !blocked && wv.popupOpenerBridgeParent != nil | |||
| 111 | + | parentURI = wv.popupOpenerBridgeParentURI | |||
| 112 | + | return parentURI, active, blocked | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | func (wv *WebView) ensureBridgeNonceLocked() string { | |||
| 116 | + | if wv == nil { | |||
| 117 | + | return "" | |||
| 118 | + | } | |||
| 119 | + | if wv.bridgeNonce != "" { | |||
| 120 | + | return wv.bridgeNonce | |||
| 121 | + | } | |||
| 122 | + | nonce := newBridgeNonce() | |||
| 123 | + | if nonce == "" { | |||
| 124 | + | return "" | |||
| 125 | + | } | |||
| 126 | + | wv.bridgeNonce = nonce | |||
| 127 | + | return nonce | |||
| 128 | + | } | |||
| 129 | + | ||||
| 130 | + | func (wv *WebView) syncPopupOpenerBridgeExtraInfoLocked() { | |||
| 131 | + | if wv == nil || wv.pendingCreate == nil || wv.pendingCreate.windowInfo == nil { | |||
| 132 | + | return | |||
| 133 | + | } | |||
| 134 | + | if wv.popupNoJavaScriptAccess || wv.popupOpenerBridgeParent == nil || wv.popupOpenerBridgeParentURI == "" { | |||
| 135 | + | wv.pendingCreate.extraInfo = nil | |||
| 136 | + | return | |||
| 137 | + | } | |||
| 138 | + | bridgeNonce := wv.ensureBridgeNonceLocked() | |||
| 139 | + | if bridgeNonce == "" { | |||
| 140 | + | wv.pendingCreate.extraInfo = nil | |||
| 141 | + | return | |||
| 142 | + | } | |||
| 143 | + | wv.pendingCreate.extraInfo = popupOpenerRenderExtraInfoBuilder(wv.popupOpenerBridgeParentURI, bridgeNonce) | |||
| 144 | + | } | |||
| 145 | + | ||||
| 103 | // EnablePopupOpenerBridge implements port.PopupOpenerCapable. | 146 | // EnablePopupOpenerBridge implements port.PopupOpenerCapable. | ||
| 104 | func (wv *WebView) EnablePopupOpenerBridge(parent port.WebView, noJavaScriptAccess bool) { | 147 | func (wv *WebView) EnablePopupOpenerBridge(parent port.WebView, noJavaScriptAccess bool) { | ||
| 105 | if wv == nil { | 148 | if wv == nil { | ||
| @@ -111,16 +154,37 @@ func (wv *WebView) EnablePopupOpenerBridge(parent port.WebView, noJavaScriptAcce | |||||
| 111 | if noJavaScriptAccess { | 154 | if noJavaScriptAccess { | ||
| 112 | wv.popupOpenerBridgeParent = nil | 155 | wv.popupOpenerBridgeParent = nil | ||
| 113 | wv.popupOpenerBridgeParentURI = "" | 156 | wv.popupOpenerBridgeParentURI = "" | ||
| 157 | + | wv.syncPopupOpenerBridgeExtraInfoLocked() | |||
| 158 | + | if wv.ctx != nil { | |||
| 159 | + | logging.FromContext(wv.ctx).Debug(). | |||
| 160 | + | Uint64("webview_id", uint64(wv.id)). | |||
| 161 | + | Bool("no_javascript_access", true). | |||
| 162 | + | Msg("cef: popup opener bridge disabled") | |||
| 163 | + | } | |||
| 114 | return | 164 | return | ||
| 115 | } | 165 | } | ||
| 116 | parentWV, ok := parent.(*WebView) | 166 | parentWV, ok := parent.(*WebView) | ||
| 117 | if !ok || parentWV == nil { | 167 | if !ok || parentWV == nil { | ||
| 118 | wv.popupOpenerBridgeParent = nil | 168 | wv.popupOpenerBridgeParent = nil | ||
| 119 | wv.popupOpenerBridgeParentURI = "" | 169 | wv.popupOpenerBridgeParentURI = "" | ||
| 170 | + | wv.syncPopupOpenerBridgeExtraInfoLocked() | |||
| 171 | + | if wv.ctx != nil { | |||
| 172 | + | logging.FromContext(wv.ctx).Debug(). | |||
| 173 | + | Uint64("webview_id", uint64(wv.id)). | |||
| 174 | + | Msg("cef: popup opener bridge unavailable: parent missing") | |||
| 175 | + | } | |||
| 120 | return | 176 | return | ||
| 121 | } | 177 | } | ||
| 122 | wv.popupOpenerBridgeParent = parentWV | 178 | wv.popupOpenerBridgeParent = parentWV | ||
| 123 | wv.popupOpenerBridgeParentURI = parentWV.URI() | 179 | wv.popupOpenerBridgeParentURI = parentWV.URI() | ||
| 180 | + | wv.syncPopupOpenerBridgeExtraInfoLocked() | |||
| 181 | + | if wv.ctx != nil { | |||
| 182 | + | logging.FromContext(wv.ctx).Debug(). | |||
| 183 | + | Uint64("webview_id", uint64(wv.id)). | |||
| 184 | + | Uint64("parent_webview_id", uint64(parentWV.id)). | |||
| 185 | + | Str("parent_uri", logging.TruncateURL(wv.popupOpenerBridgeParentURI, logging.PermissionLogURLMaxLen)). | |||
| 186 | + | Msg("cef: popup opener bridge enabled") | |||
| 187 | + | } | |||
| 124 | } | 188 | } | ||
| 125 | 189 | ||||
| 126 | func (wv *WebView) popupOpenerBridgeScript(bridgeNonce string) string { | 190 | func (wv *WebView) popupOpenerBridgeScript(bridgeNonce string) string { | ||
| @@ -131,11 +195,26 @@ func (wv *WebView) popupOpenerBridgeScript(bridgeNonce string) string { | |||||
| 131 | wv.mu.RLock() | 195 | wv.mu.RLock() | ||
| 132 | parentURI := wv.popupOpenerBridgeParentURI | 196 | parentURI := wv.popupOpenerBridgeParentURI | ||
| 133 | parent := wv.popupOpenerBridgeParent | 197 | parent := wv.popupOpenerBridgeParent | ||
| 134 | - | blocked := wv.popupNoJavaScriptAccess || parent == nil | 198 | + | noJavaScriptAccess := wv.popupNoJavaScriptAccess |
| 199 | + | blocked := noJavaScriptAccess || parent == nil | |||
| 135 | wv.mu.RUnlock() | 200 | wv.mu.RUnlock() | ||
| 136 | if blocked { | 201 | if blocked { | ||
| 202 | + | if wv.ctx != nil { | |||
| 203 | + | logging.FromContext(wv.ctx).Debug(). | |||
| 204 | + | Uint64("webview_id", uint64(wv.id)). | |||
| 205 | + | Bool("blocked", true). | |||
| 206 | + | Bool("has_parent", parent != nil). | |||
| 207 | + | Bool("no_javascript_access", noJavaScriptAccess). | |||
| 208 | + | Msg("cef: popup opener bridge script skipped") | |||
| 209 | + | } | |||
| 137 | return "" | 210 | return "" | ||
| 138 | } | 211 | } | ||
| 212 | + | if wv.ctx != nil { | |||
| 213 | + | logging.FromContext(wv.ctx).Debug(). | |||
| 214 | + | Uint64("webview_id", uint64(wv.id)). | |||
| 215 | + | Str("parent_uri", logging.TruncateURL(parentURI, logging.PermissionLogURLMaxLen)). | |||
| 216 | + | Msg("cef: popup opener bridge script prepared") | |||
| 217 | + | } | |||
| 139 | 218 | ||||
| 140 | return buildPopupOpenerBridgeJS(bridgeNonce, parentURI) | 219 | return buildPopupOpenerBridgeJS(bridgeNonce, parentURI) | ||
| 141 | } | 220 | } | ||
| @@ -154,13 +233,32 @@ func (wv *WebView) handlePopupOpenerNavigate(targetURL string) { | |||||
| 154 | opener := wv.popupOpenerBridgeParent | 233 | opener := wv.popupOpenerBridgeParent | ||
| 155 | wv.mu.RUnlock() | 234 | wv.mu.RUnlock() | ||
| 156 | if opener == nil || opener.destroyed.Load() { | 235 | if opener == nil || opener.destroyed.Load() { | ||
| 236 | + | if wv.ctx != nil { | |||
| 237 | + | logging.FromContext(wv.ctx).Debug(). | |||
| 238 | + | Uint64("webview_id", uint64(wv.id)). | |||
| 239 | + | Str("target_url", logging.TruncateURL(trimmedURL, logging.PermissionLogURLMaxLen)). | |||
| 240 | + | Msg("cef: popup opener navigate ignored: opener unavailable") | |||
| 241 | + | } | |||
| 157 | return | 242 | return | ||
| 158 | } | 243 | } | ||
| 159 | 244 | ||||
| 160 | resolvedURL := resolvePopupOpenerNavigationTarget(trimmedURL, opener.URI()) | 245 | resolvedURL := resolvePopupOpenerNavigationTarget(trimmedURL, opener.URI()) | ||
| 161 | if resolvedURL == "" { | 246 | if resolvedURL == "" { | ||
| 247 | + | if wv.ctx != nil { | |||
| 248 | + | logging.FromContext(wv.ctx).Warn(). | |||
| 249 | + | Uint64("webview_id", uint64(wv.id)). | |||
| 250 | + | Str("target_url", logging.TruncateURL(trimmedURL, logging.PermissionLogURLMaxLen)). | |||
| 251 | + | Msg("cef: popup opener navigate rejected") | |||
| 252 | + | } | |||
| 162 | return | 253 | return | ||
| 163 | } | 254 | } | ||
| 255 | + | if wv.ctx != nil { | |||
| 256 | + | logging.FromContext(wv.ctx).Debug(). | |||
| 257 | + | Uint64("webview_id", uint64(wv.id)). | |||
| 258 | + | Str("target_url", logging.TruncateURL(trimmedURL, logging.PermissionLogURLMaxLen)). | |||
| 259 | + | Str("resolved_url", logging.TruncateURL(resolvedURL, logging.PermissionLogURLMaxLen)). | |||
| 260 | + | Msg("cef: popup opener navigate received") | |||
| 261 | + | } | |||
| 164 | 262 | ||||
| 165 | if err := opener.LoadURI(context.Background(), resolvedURL); err != nil && opener.ctx != nil { | 263 | if err := opener.LoadURI(context.Background(), resolvedURL); err != nil && opener.ctx != nil { | ||
| 166 | logging.FromContext(opener.ctx).Warn(). | 264 | logging.FromContext(opener.ctx).Warn(). | ||
| @@ -224,11 +322,32 @@ func (wv *WebView) handlePopupOpenerPostMessage(payload popupOpenerPostMessagePa | |||||
| 224 | opener := wv.popupOpenerBridgeParent | 322 | opener := wv.popupOpenerBridgeParent | ||
| 225 | wv.mu.RUnlock() | 323 | wv.mu.RUnlock() | ||
| 226 | if opener == nil || opener.destroyed.Load() { | 324 | if opener == nil || opener.destroyed.Load() { | ||
| 325 | + | if wv.ctx != nil { | |||
| 326 | + | logging.FromContext(wv.ctx).Debug(). | |||
| 327 | + | Uint64("webview_id", uint64(wv.id)). | |||
| 328 | + | Str("target_origin", payload.TargetOrigin). | |||
| 329 | + | Msg("cef: popup opener postMessage ignored: opener unavailable") | |||
| 330 | + | } | |||
| 227 | return | 331 | return | ||
| 228 | } | 332 | } | ||
| 229 | if !targetOriginMatchesPopupOpener(payload.TargetOrigin, opener.URI()) { | 333 | if !targetOriginMatchesPopupOpener(payload.TargetOrigin, opener.URI()) { | ||
| 334 | + | if wv.ctx != nil { | |||
| 335 | + | logging.FromContext(wv.ctx).Warn(). | |||
| 336 | + | Uint64("webview_id", uint64(wv.id)). | |||
| 337 | + | Str("target_origin", payload.TargetOrigin). | |||
| 338 | + | Str("opener_uri", logging.TruncateURL(opener.URI(), logging.PermissionLogURLMaxLen)). | |||
| 339 | + | Msg("cef: popup opener postMessage rejected") | |||
| 340 | + | } | |||
| 230 | return | 341 | return | ||
| 231 | } | 342 | } | ||
| 343 | + | if wv.ctx != nil { | |||
| 344 | + | logging.FromContext(wv.ctx).Debug(). | |||
| 345 | + | Uint64("webview_id", uint64(wv.id)). | |||
| 346 | + | Str("target_origin", payload.TargetOrigin). | |||
| 347 | + | Str("source_origin", payload.SourceOrigin). | |||
| 348 | + | Str("source_href", logging.TruncateURL(payload.SourceHref, logging.PermissionLogURLMaxLen)). | |||
| 349 | + | Msg("cef: popup opener postMessage received") | |||
| 350 | + | } | |||
| 232 | 351 | ||||
| 233 | sourceOrigin := popupSourceOrigin(payload) | 352 | sourceOrigin := popupSourceOrigin(payload) | ||
| 234 | dataExpr := "undefined" | 353 | dataExpr := "undefined" | ||
A internal/infrastructure/cef/popup_opener_renderer.go +142 -0
| @@ -0,0 +1,142 @@ | |||
| 1 | + | package cef | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "sync" | |
| 6 | + | ||
| 7 | + | purecef "github.com/bnema/purego-cef/cef" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/dumber/internal/logging" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | const ( | |
| 13 | + | popupOpenerExtraInfoEnabledKey = "dumber.popup_opener.enabled" | |
| 14 | + | popupOpenerExtraInfoParentURIKey = "dumber.popup_opener.parent_uri" | |
| 15 | + | popupOpenerExtraInfoBridgeNonceKey = "dumber.popup_opener.bridge_nonce" | |
| 16 | + | popupOpenerRenderScriptURL = "dumber://popup-opener-renderer" | |
| 17 | + | ) | |
| 18 | + | ||
| 19 | + | type popupOpenerRenderMetadata struct { | |
| 20 | + | ParentURI string | |
| 21 | + | BridgeNonce string | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | var popupOpenerRenderExtraInfoBuilder = buildPopupOpenerRenderExtraInfo | |
| 25 | + | ||
| 26 | + | func buildPopupOpenerRenderExtraInfo(parentURI, bridgeNonce string) purecef.DictionaryValue { | |
| 27 | + | if parentURI == "" || bridgeNonce == "" { | |
| 28 | + | return nil | |
| 29 | + | } | |
| 30 | + | extraInfo := purecef.DictionaryValueCreate() | |
| 31 | + | if extraInfo == nil { | |
| 32 | + | return nil | |
| 33 | + | } | |
| 34 | + | extraInfo.SetBool(popupOpenerExtraInfoEnabledKey, 1) | |
| 35 | + | extraInfo.SetString(popupOpenerExtraInfoParentURIKey, parentURI) | |
| 36 | + | extraInfo.SetString(popupOpenerExtraInfoBridgeNonceKey, bridgeNonce) | |
| 37 | + | return extraInfo | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | func decodePopupOpenerRenderMetadata(extraInfo purecef.DictionaryValue) (popupOpenerRenderMetadata, bool) { | |
| 41 | + | var metadata popupOpenerRenderMetadata | |
| 42 | + | if extraInfo == nil || extraInfo.GetBool(popupOpenerExtraInfoEnabledKey) != 1 { | |
| 43 | + | return metadata, false | |
| 44 | + | } | |
| 45 | + | metadata.ParentURI = extraInfo.GetString(popupOpenerExtraInfoParentURIKey) | |
| 46 | + | metadata.BridgeNonce = extraInfo.GetString(popupOpenerExtraInfoBridgeNonceKey) | |
| 47 | + | if metadata.ParentURI == "" || metadata.BridgeNonce == "" { | |
| 48 | + | return popupOpenerRenderMetadata{}, false | |
| 49 | + | } | |
| 50 | + | return metadata, true | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | type popupOpenerRenderProcessHandler struct { | |
| 54 | + | mu sync.RWMutex | |
| 55 | + | byBrowserID map[int32]popupOpenerRenderMetadata | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | func newPopupOpenerRenderProcessHandler() *popupOpenerRenderProcessHandler { | |
| 59 | + | return &popupOpenerRenderProcessHandler{byBrowserID: make(map[int32]popupOpenerRenderMetadata)} | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | func (*popupOpenerRenderProcessHandler) OnWebKitInitialized() {} | |
| 63 | + | ||
| 64 | + | func (h *popupOpenerRenderProcessHandler) OnBrowserCreated(browser purecef.Browser, extraInfo purecef.DictionaryValue) { | |
| 65 | + | if h == nil || browser == nil { | |
| 66 | + | return | |
| 67 | + | } | |
| 68 | + | metadata, ok := decodePopupOpenerRenderMetadata(extraInfo) | |
| 69 | + | if !ok { | |
| 70 | + | return | |
| 71 | + | } | |
| 72 | + | browserID := browser.GetIdentifier() | |
| 73 | + | h.mu.Lock() | |
| 74 | + | h.byBrowserID[browserID] = metadata | |
| 75 | + | h.mu.Unlock() | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func (h *popupOpenerRenderProcessHandler) OnBrowserDestroyed(browser purecef.Browser) { | |
| 79 | + | if h == nil || browser == nil { | |
| 80 | + | return | |
| 81 | + | } | |
| 82 | + | browserID := browser.GetIdentifier() | |
| 83 | + | h.mu.Lock() | |
| 84 | + | delete(h.byBrowserID, browserID) | |
| 85 | + | h.mu.Unlock() | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | func (*popupOpenerRenderProcessHandler) GetLoadHandler() purecef.LoadHandler { return nil } | |
| 89 | + | ||
| 90 | + | func (h *popupOpenerRenderProcessHandler) OnContextCreated(browser purecef.Browser, frame purecef.Frame, ctx purecef.V8Context) { | |
| 91 | + | if h == nil || browser == nil || frame == nil || !frame.IsMain() || ctx == nil || !ctx.IsValid() { | |
| 92 | + | return | |
| 93 | + | } | |
| 94 | + | browserID := browser.GetIdentifier() | |
| 95 | + | h.mu.RLock() | |
| 96 | + | metadata, ok := h.byBrowserID[browserID] | |
| 97 | + | h.mu.RUnlock() | |
| 98 | + | if !ok { | |
| 99 | + | return | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | script := buildPopupOpenerBridgeJS(metadata.BridgeNonce, metadata.ParentURI) | |
| 103 | + | if script == "" { | |
| 104 | + | return | |
| 105 | + | } | |
| 106 | + | if ctx.Enter() != 1 { | |
| 107 | + | logging.FromContext(context.Background()).Warn(). | |
| 108 | + | Int32("browser_id", browserID). | |
| 109 | + | Msg("cef: popup opener renderer bridge could not enter V8 context") | |
| 110 | + | return | |
| 111 | + | } | |
| 112 | + | defer ctx.Exit() | |
| 113 | + | if ctx.Eval(script, popupOpenerRenderScriptURL, 0, nil, nil) != 1 { | |
| 114 | + | logging.FromContext(context.Background()).Warn(). | |
| 115 | + | Int32("browser_id", browserID). | |
| 116 | + | Msg("cef: popup opener renderer bridge eval failed") | |
| 117 | + | } | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | func (*popupOpenerRenderProcessHandler) OnContextReleased(purecef.Browser, purecef.Frame, purecef.V8Context) { | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | func (*popupOpenerRenderProcessHandler) OnUncaughtException( | |
| 124 | + | purecef.Browser, | |
| 125 | + | purecef.Frame, | |
| 126 | + | purecef.V8Context, | |
| 127 | + | purecef.V8Exception, | |
| 128 | + | purecef.V8StackTrace, | |
| 129 | + | ) { | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | func (*popupOpenerRenderProcessHandler) OnFocusedNodeChanged(purecef.Browser, purecef.Frame, purecef.Domnode) { | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | func (*popupOpenerRenderProcessHandler) OnProcessMessageReceived( | |
| 136 | + | purecef.Browser, | |
| 137 | + | purecef.Frame, | |
| 138 | + | purecef.ProcessID, | |
| 139 | + | purecef.ProcessMessage, | |
| 140 | + | ) int32 { | |
| 141 | + | return 0 | |
| 142 | + | } | |
| @@ -0,0 +1,142 @@ | |||||
| 1 | + | package cef | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "sync" | |||
| 6 | + | ||||
| 7 | + | purecef "github.com/bnema/purego-cef/cef" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/dumber/internal/logging" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | const ( | |||
| 13 | + | popupOpenerExtraInfoEnabledKey = "dumber.popup_opener.enabled" | |||
| 14 | + | popupOpenerExtraInfoParentURIKey = "dumber.popup_opener.parent_uri" | |||
| 15 | + | popupOpenerExtraInfoBridgeNonceKey = "dumber.popup_opener.bridge_nonce" | |||
| 16 | + | popupOpenerRenderScriptURL = "dumber://popup-opener-renderer" | |||
| 17 | + | ) | |||
| 18 | + | ||||
| 19 | + | type popupOpenerRenderMetadata struct { | |||
| 20 | + | ParentURI string | |||
| 21 | + | BridgeNonce string | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | var popupOpenerRenderExtraInfoBuilder = buildPopupOpenerRenderExtraInfo | |||
| 25 | + | ||||
| 26 | + | func buildPopupOpenerRenderExtraInfo(parentURI, bridgeNonce string) purecef.DictionaryValue { | |||
| 27 | + | if parentURI == "" || bridgeNonce == "" { | |||
| 28 | + | return nil | |||
| 29 | + | } | |||
| 30 | + | extraInfo := purecef.DictionaryValueCreate() | |||
| 31 | + | if extraInfo == nil { | |||
| 32 | + | return nil | |||
| 33 | + | } | |||
| 34 | + | extraInfo.SetBool(popupOpenerExtraInfoEnabledKey, 1) | |||
| 35 | + | extraInfo.SetString(popupOpenerExtraInfoParentURIKey, parentURI) | |||
| 36 | + | extraInfo.SetString(popupOpenerExtraInfoBridgeNonceKey, bridgeNonce) | |||
| 37 | + | return extraInfo | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | func decodePopupOpenerRenderMetadata(extraInfo purecef.DictionaryValue) (popupOpenerRenderMetadata, bool) { | |||
| 41 | + | var metadata popupOpenerRenderMetadata | |||
| 42 | + | if extraInfo == nil || extraInfo.GetBool(popupOpenerExtraInfoEnabledKey) != 1 { | |||
| 43 | + | return metadata, false | |||
| 44 | + | } | |||
| 45 | + | metadata.ParentURI = extraInfo.GetString(popupOpenerExtraInfoParentURIKey) | |||
| 46 | + | metadata.BridgeNonce = extraInfo.GetString(popupOpenerExtraInfoBridgeNonceKey) | |||
| 47 | + | if metadata.ParentURI == "" || metadata.BridgeNonce == "" { | |||
| 48 | + | return popupOpenerRenderMetadata{}, false | |||
| 49 | + | } | |||
| 50 | + | return metadata, true | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | type popupOpenerRenderProcessHandler struct { | |||
| 54 | + | mu sync.RWMutex | |||
| 55 | + | byBrowserID map[int32]popupOpenerRenderMetadata | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | func newPopupOpenerRenderProcessHandler() *popupOpenerRenderProcessHandler { | |||
| 59 | + | return &popupOpenerRenderProcessHandler{byBrowserID: make(map[int32]popupOpenerRenderMetadata)} | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | func (*popupOpenerRenderProcessHandler) OnWebKitInitialized() {} | |||
| 63 | + | ||||
| 64 | + | func (h *popupOpenerRenderProcessHandler) OnBrowserCreated(browser purecef.Browser, extraInfo purecef.DictionaryValue) { | |||
| 65 | + | if h == nil || browser == nil { | |||
| 66 | + | return | |||
| 67 | + | } | |||
| 68 | + | metadata, ok := decodePopupOpenerRenderMetadata(extraInfo) | |||
| 69 | + | if !ok { | |||
| 70 | + | return | |||
| 71 | + | } | |||
| 72 | + | browserID := browser.GetIdentifier() | |||
| 73 | + | h.mu.Lock() | |||
| 74 | + | h.byBrowserID[browserID] = metadata | |||
| 75 | + | h.mu.Unlock() | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func (h *popupOpenerRenderProcessHandler) OnBrowserDestroyed(browser purecef.Browser) { | |||
| 79 | + | if h == nil || browser == nil { | |||
| 80 | + | return | |||
| 81 | + | } | |||
| 82 | + | browserID := browser.GetIdentifier() | |||
| 83 | + | h.mu.Lock() | |||
| 84 | + | delete(h.byBrowserID, browserID) | |||
| 85 | + | h.mu.Unlock() | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | func (*popupOpenerRenderProcessHandler) GetLoadHandler() purecef.LoadHandler { return nil } | |||
| 89 | + | ||||
| 90 | + | func (h *popupOpenerRenderProcessHandler) OnContextCreated(browser purecef.Browser, frame purecef.Frame, ctx purecef.V8Context) { | |||
| 91 | + | if h == nil || browser == nil || frame == nil || !frame.IsMain() || ctx == nil || !ctx.IsValid() { | |||
| 92 | + | return | |||
| 93 | + | } | |||
| 94 | + | browserID := browser.GetIdentifier() | |||
| 95 | + | h.mu.RLock() | |||
| 96 | + | metadata, ok := h.byBrowserID[browserID] | |||
| 97 | + | h.mu.RUnlock() | |||
| 98 | + | if !ok { | |||
| 99 | + | return | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | script := buildPopupOpenerBridgeJS(metadata.BridgeNonce, metadata.ParentURI) | |||
| 103 | + | if script == "" { | |||
| 104 | + | return | |||
| 105 | + | } | |||
| 106 | + | if ctx.Enter() != 1 { | |||
| 107 | + | logging.FromContext(context.Background()).Warn(). | |||
| 108 | + | Int32("browser_id", browserID). | |||
| 109 | + | Msg("cef: popup opener renderer bridge could not enter V8 context") | |||
| 110 | + | return | |||
| 111 | + | } | |||
| 112 | + | defer ctx.Exit() | |||
| 113 | + | if ctx.Eval(script, popupOpenerRenderScriptURL, 0, nil, nil) != 1 { | |||
| 114 | + | logging.FromContext(context.Background()).Warn(). | |||
| 115 | + | Int32("browser_id", browserID). | |||
| 116 | + | Msg("cef: popup opener renderer bridge eval failed") | |||
| 117 | + | } | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | func (*popupOpenerRenderProcessHandler) OnContextReleased(purecef.Browser, purecef.Frame, purecef.V8Context) { | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | func (*popupOpenerRenderProcessHandler) OnUncaughtException( | |||
| 124 | + | purecef.Browser, | |||
| 125 | + | purecef.Frame, | |||
| 126 | + | purecef.V8Context, | |||
| 127 | + | purecef.V8Exception, | |||
| 128 | + | purecef.V8StackTrace, | |||
| 129 | + | ) { | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | func (*popupOpenerRenderProcessHandler) OnFocusedNodeChanged(purecef.Browser, purecef.Frame, purecef.Domnode) { | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | func (*popupOpenerRenderProcessHandler) OnProcessMessageReceived( | |||
| 136 | + | purecef.Browser, | |||
| 137 | + | purecef.Frame, | |||
| 138 | + | purecef.ProcessID, | |||
| 139 | + | purecef.ProcessMessage, | |||
| 140 | + | ) int32 { | |||
| 141 | + | return 0 | |||
| 142 | + | } | |||
A internal/infrastructure/cef/popup_opener_renderer_test.go +160 -0
| @@ -0,0 +1,160 @@ | |||
| 1 | + | package cef | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | "unsafe" | |
| 6 | + | ||
| 7 | + | purecef "github.com/bnema/purego-cef/cef" | |
| 8 | + | cefmocks "github.com/bnema/purego-cef/cef/mocks" | |
| 9 | + | "github.com/stretchr/testify/require" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | type testPopupOpenerDictionaryValue struct { | |
| 13 | + | strings map[string]string | |
| 14 | + | bools map[string]int32 | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | func (v testPopupOpenerDictionaryValue) IsValid() bool { return true } | |
| 18 | + | func (v testPopupOpenerDictionaryValue) IsOwned() bool { return true } | |
| 19 | + | func (v testPopupOpenerDictionaryValue) IsReadOnly() bool { return false } | |
| 20 | + | func (v testPopupOpenerDictionaryValue) IsSame(purecef.DictionaryValue) bool { return false } | |
| 21 | + | func (v testPopupOpenerDictionaryValue) IsEqual(purecef.DictionaryValue) bool { return false } | |
| 22 | + | func (v testPopupOpenerDictionaryValue) Copy(int32) purecef.DictionaryValue { return v } | |
| 23 | + | func (v testPopupOpenerDictionaryValue) GetSize() int { return len(v.strings) + len(v.bools) } | |
| 24 | + | func (v testPopupOpenerDictionaryValue) Clear() int32 { return 1 } | |
| 25 | + | func (v testPopupOpenerDictionaryValue) HasKey(key string) bool { | |
| 26 | + | _, ok := v.strings[key] | |
| 27 | + | if ok { | |
| 28 | + | return true | |
| 29 | + | } | |
| 30 | + | _, ok = v.bools[key] | |
| 31 | + | return ok | |
| 32 | + | } | |
| 33 | + | func (v testPopupOpenerDictionaryValue) GetKeys(purecef.StringList) int32 { return 0 } | |
| 34 | + | func (v testPopupOpenerDictionaryValue) Remove(string) int32 { return 1 } | |
| 35 | + | func (v testPopupOpenerDictionaryValue) GetType(string) purecef.ValueType { return 0 } | |
| 36 | + | func (v testPopupOpenerDictionaryValue) GetValue(string) purecef.Value { return nil } | |
| 37 | + | func (v testPopupOpenerDictionaryValue) GetBool(key string) int32 { return v.bools[key] } | |
| 38 | + | func (v testPopupOpenerDictionaryValue) GetInt(string) int32 { return 0 } | |
| 39 | + | func (v testPopupOpenerDictionaryValue) GetDouble(string) float64 { return 0 } | |
| 40 | + | func (v testPopupOpenerDictionaryValue) GetString(key string) string { return v.strings[key] } | |
| 41 | + | func (v testPopupOpenerDictionaryValue) GetBinary(string) purecef.BinaryValue { return nil } | |
| 42 | + | func (v testPopupOpenerDictionaryValue) GetDictionary(string) purecef.DictionaryValue { return nil } | |
| 43 | + | func (v testPopupOpenerDictionaryValue) GetList(string) purecef.ListValue { return nil } | |
| 44 | + | func (v testPopupOpenerDictionaryValue) SetValue(string, purecef.Value) int32 { return 1 } | |
| 45 | + | func (v testPopupOpenerDictionaryValue) SetNull(string) int32 { return 1 } | |
| 46 | + | func (v testPopupOpenerDictionaryValue) SetBool(string, int32) int32 { return 1 } | |
| 47 | + | func (v testPopupOpenerDictionaryValue) SetInt(string, int32) int32 { return 1 } | |
| 48 | + | func (v testPopupOpenerDictionaryValue) SetDouble(string, float64) int32 { return 1 } | |
| 49 | + | func (v testPopupOpenerDictionaryValue) SetString(string, string) int32 { return 1 } | |
| 50 | + | func (v testPopupOpenerDictionaryValue) SetBinary(string, purecef.BinaryValue) int32 { return 1 } | |
| 51 | + | func (v testPopupOpenerDictionaryValue) SetDictionary(string, purecef.DictionaryValue) int32 { | |
| 52 | + | return 1 | |
| 53 | + | } | |
| 54 | + | func (v testPopupOpenerDictionaryValue) SetList(string, purecef.ListValue) int32 { return 1 } | |
| 55 | + | ||
| 56 | + | type testPopupOpenerV8Context struct { | |
| 57 | + | valid bool | |
| 58 | + | evalCode string | |
| 59 | + | evalURL string | |
| 60 | + | enterCalls int | |
| 61 | + | exitCalls int | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | func (c *testPopupOpenerV8Context) GetTaskRunner() purecef.TaskRunner { return nil } | |
| 65 | + | func (c *testPopupOpenerV8Context) IsValid() bool { return c.valid } | |
| 66 | + | func (c *testPopupOpenerV8Context) GetBrowser() purecef.Browser { return nil } | |
| 67 | + | func (c *testPopupOpenerV8Context) GetFrame() purecef.Frame { return nil } | |
| 68 | + | func (c *testPopupOpenerV8Context) GetGlobal() purecef.V8Value { return nil } | |
| 69 | + | func (c *testPopupOpenerV8Context) Enter() int32 { | |
| 70 | + | c.enterCalls++ | |
| 71 | + | return 1 | |
| 72 | + | } | |
| 73 | + | func (c *testPopupOpenerV8Context) Exit() int32 { | |
| 74 | + | c.exitCalls++ | |
| 75 | + | return 1 | |
| 76 | + | } | |
| 77 | + | func (c *testPopupOpenerV8Context) IsSame(that purecef.V8Context) bool { return c == that } | |
| 78 | + | func (c *testPopupOpenerV8Context) Eval(code string, scriptURL string, _ int32, _, _ unsafe.Pointer) int32 { | |
| 79 | + | c.evalCode = code | |
| 80 | + | c.evalURL = scriptURL | |
| 81 | + | return 1 | |
| 82 | + | } | |
| 83 | + | func (c *testPopupOpenerV8Context) RawPointer() unsafe.Pointer { return nil } | |
| 84 | + | func (c *testPopupOpenerV8Context) Release() {} | |
| 85 | + | ||
| 86 | + | func TestDecodePopupOpenerRenderMetadata(t *testing.T) { | |
| 87 | + | metadata, ok := decodePopupOpenerRenderMetadata(testPopupOpenerDictionaryValue{ | |
| 88 | + | strings: map[string]string{ | |
| 89 | + | popupOpenerExtraInfoParentURIKey: "https://example.com/login", | |
| 90 | + | popupOpenerExtraInfoBridgeNonceKey: "bridge-nonce", | |
| 91 | + | }, | |
| 92 | + | bools: map[string]int32{ | |
| 93 | + | popupOpenerExtraInfoEnabledKey: 1, | |
| 94 | + | }, | |
| 95 | + | }) | |
| 96 | + | ||
| 97 | + | require.True(t, ok) | |
| 98 | + | require.Equal(t, "https://example.com/login", metadata.ParentURI) | |
| 99 | + | require.Equal(t, "bridge-nonce", metadata.BridgeNonce) | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | func TestPopupOpenerRenderProcessHandler_OnContextCreatedInstallsBridgeForConfiguredMainFrame(t *testing.T) { | |
| 103 | + | h := newPopupOpenerRenderProcessHandler() | |
| 104 | + | browser := cefmocks.NewMockBrowser(t) | |
| 105 | + | browser.EXPECT().GetIdentifier().Return(int32(42)).Twice() | |
| 106 | + | ctx := &testPopupOpenerV8Context{valid: true} | |
| 107 | + | ||
| 108 | + | h.OnBrowserCreated(browser, testPopupOpenerDictionaryValue{ | |
| 109 | + | strings: map[string]string{ | |
| 110 | + | popupOpenerExtraInfoParentURIKey: "https://example.com/login", | |
| 111 | + | popupOpenerExtraInfoBridgeNonceKey: "bridge-nonce", | |
| 112 | + | }, | |
| 113 | + | bools: map[string]int32{ | |
| 114 | + | popupOpenerExtraInfoEnabledKey: 1, | |
| 115 | + | }, | |
| 116 | + | }) | |
| 117 | + | h.OnContextCreated(browser, stubFrame{main: true, url: "https://example.com/popup"}, ctx) | |
| 118 | + | ||
| 119 | + | require.Equal(t, 1, ctx.enterCalls) | |
| 120 | + | require.Equal(t, 1, ctx.exitCalls) | |
| 121 | + | require.Contains(t, ctx.evalCode, "popup-opener-navigate") | |
| 122 | + | require.Contains(t, ctx.evalCode, "popup-opener-post-message") | |
| 123 | + | require.Contains(t, ctx.evalCode, "https://example.com/login") | |
| 124 | + | require.Contains(t, ctx.evalCode, "bridge-nonce") | |
| 125 | + | require.Equal(t, popupOpenerRenderScriptURL, ctx.evalURL) | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | func TestPopupOpenerRenderProcessHandler_OnContextCreatedSkipsWithoutMetadata(t *testing.T) { | |
| 129 | + | h := newPopupOpenerRenderProcessHandler() | |
| 130 | + | browser := cefmocks.NewMockBrowser(t) | |
| 131 | + | browser.EXPECT().GetIdentifier().Return(int32(7)).Once() | |
| 132 | + | ctx := &testPopupOpenerV8Context{valid: true} | |
| 133 | + | ||
| 134 | + | h.OnContextCreated(browser, stubFrame{main: true, url: "https://example.com/popup"}, ctx) | |
| 135 | + | ||
| 136 | + | require.Zero(t, ctx.enterCalls) | |
| 137 | + | require.Empty(t, ctx.evalCode) | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | func TestEnablePopupOpenerBridge_SyncsPendingCreateExtraInfo(t *testing.T) { | |
| 141 | + | oldBuilder := popupOpenerRenderExtraInfoBuilder | |
| 142 | + | popupOpenerRenderExtraInfoBuilder = func(parentURI, bridgeNonce string) purecef.DictionaryValue { | |
| 143 | + | require.Equal(t, "https://example.com/login", parentURI) | |
| 144 | + | require.NotEmpty(t, bridgeNonce) | |
| 145 | + | return testPopupOpenerDictionaryValue{strings: map[string]string{"ok": "1"}} | |
| 146 | + | } | |
| 147 | + | t.Cleanup(func() { | |
| 148 | + | popupOpenerRenderExtraInfoBuilder = oldBuilder | |
| 149 | + | }) | |
| 150 | + | ||
| 151 | + | parent := &WebView{} | |
| 152 | + | parent.updateURI("https://example.com/login") | |
| 153 | + | windowInfo := purecef.NewWindowInfo() | |
| 154 | + | wv := &WebView{pendingCreate: &pendingBrowserCreate{windowInfo: &windowInfo}} | |
| 155 | + | ||
| 156 | + | wv.EnablePopupOpenerBridge(parent, false) | |
| 157 | + | ||
| 158 | + | require.NotNil(t, wv.pendingCreate.extraInfo) | |
| 159 | + | require.NotEmpty(t, wv.bridgeNonce) | |
| 160 | + | } | |
| @@ -0,0 +1,160 @@ | |||||
| 1 | + | package cef | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | "unsafe" | |||
| 6 | + | ||||
| 7 | + | purecef "github.com/bnema/purego-cef/cef" | |||
| 8 | + | cefmocks "github.com/bnema/purego-cef/cef/mocks" | |||
| 9 | + | "github.com/stretchr/testify/require" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | type testPopupOpenerDictionaryValue struct { | |||
| 13 | + | strings map[string]string | |||
| 14 | + | bools map[string]int32 | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | func (v testPopupOpenerDictionaryValue) IsValid() bool { return true } | |||
| 18 | + | func (v testPopupOpenerDictionaryValue) IsOwned() bool { return true } | |||
| 19 | + | func (v testPopupOpenerDictionaryValue) IsReadOnly() bool { return false } | |||
| 20 | + | func (v testPopupOpenerDictionaryValue) IsSame(purecef.DictionaryValue) bool { return false } | |||
| 21 | + | func (v testPopupOpenerDictionaryValue) IsEqual(purecef.DictionaryValue) bool { return false } | |||
| 22 | + | func (v testPopupOpenerDictionaryValue) Copy(int32) purecef.DictionaryValue { return v } | |||
| 23 | + | func (v testPopupOpenerDictionaryValue) GetSize() int { return len(v.strings) + len(v.bools) } | |||
| 24 | + | func (v testPopupOpenerDictionaryValue) Clear() int32 { return 1 } | |||
| 25 | + | func (v testPopupOpenerDictionaryValue) HasKey(key string) bool { | |||
| 26 | + | _, ok := v.strings[key] | |||
| 27 | + | if ok { | |||
| 28 | + | return true | |||
| 29 | + | } | |||
| 30 | + | _, ok = v.bools[key] | |||
| 31 | + | return ok | |||
| 32 | + | } | |||
| 33 | + | func (v testPopupOpenerDictionaryValue) GetKeys(purecef.StringList) int32 { return 0 } | |||
| 34 | + | func (v testPopupOpenerDictionaryValue) Remove(string) int32 { return 1 } | |||
| 35 | + | func (v testPopupOpenerDictionaryValue) GetType(string) purecef.ValueType { return 0 } | |||
| 36 | + | func (v testPopupOpenerDictionaryValue) GetValue(string) purecef.Value { return nil } | |||
| 37 | + | func (v testPopupOpenerDictionaryValue) GetBool(key string) int32 { return v.bools[key] } | |||
| 38 | + | func (v testPopupOpenerDictionaryValue) GetInt(string) int32 { return 0 } | |||
| 39 | + | func (v testPopupOpenerDictionaryValue) GetDouble(string) float64 { return 0 } | |||
| 40 | + | func (v testPopupOpenerDictionaryValue) GetString(key string) string { return v.strings[key] } | |||
| 41 | + | func (v testPopupOpenerDictionaryValue) GetBinary(string) purecef.BinaryValue { return nil } | |||
| 42 | + | func (v testPopupOpenerDictionaryValue) GetDictionary(string) purecef.DictionaryValue { return nil } | |||
| 43 | + | func (v testPopupOpenerDictionaryValue) GetList(string) purecef.ListValue { return nil } | |||
| 44 | + | func (v testPopupOpenerDictionaryValue) SetValue(string, purecef.Value) int32 { return 1 } | |||
| 45 | + | func (v testPopupOpenerDictionaryValue) SetNull(string) int32 { return 1 } | |||
| 46 | + | func (v testPopupOpenerDictionaryValue) SetBool(string, int32) int32 { return 1 } | |||
| 47 | + | func (v testPopupOpenerDictionaryValue) SetInt(string, int32) int32 { return 1 } | |||
| 48 | + | func (v testPopupOpenerDictionaryValue) SetDouble(string, float64) int32 { return 1 } | |||
| 49 | + | func (v testPopupOpenerDictionaryValue) SetString(string, string) int32 { return 1 } | |||
| 50 | + | func (v testPopupOpenerDictionaryValue) SetBinary(string, purecef.BinaryValue) int32 { return 1 } | |||
| 51 | + | func (v testPopupOpenerDictionaryValue) SetDictionary(string, purecef.DictionaryValue) int32 { | |||
| 52 | + | return 1 | |||
| 53 | + | } | |||
| 54 | + | func (v testPopupOpenerDictionaryValue) SetList(string, purecef.ListValue) int32 { return 1 } | |||
| 55 | + | ||||
| 56 | + | type testPopupOpenerV8Context struct { | |||
| 57 | + | valid bool | |||
| 58 | + | evalCode string | |||
| 59 | + | evalURL string | |||
| 60 | + | enterCalls int | |||
| 61 | + | exitCalls int | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | func (c *testPopupOpenerV8Context) GetTaskRunner() purecef.TaskRunner { return nil } | |||
| 65 | + | func (c *testPopupOpenerV8Context) IsValid() bool { return c.valid } | |||
| 66 | + | func (c *testPopupOpenerV8Context) GetBrowser() purecef.Browser { return nil } | |||
| 67 | + | func (c *testPopupOpenerV8Context) GetFrame() purecef.Frame { return nil } | |||
| 68 | + | func (c *testPopupOpenerV8Context) GetGlobal() purecef.V8Value { return nil } | |||
| 69 | + | func (c *testPopupOpenerV8Context) Enter() int32 { | |||
| 70 | + | c.enterCalls++ | |||
| 71 | + | return 1 | |||
| 72 | + | } | |||
| 73 | + | func (c *testPopupOpenerV8Context) Exit() int32 { | |||
| 74 | + | c.exitCalls++ | |||
| 75 | + | return 1 | |||
| 76 | + | } | |||
| 77 | + | func (c *testPopupOpenerV8Context) IsSame(that purecef.V8Context) bool { return c == that } | |||
| 78 | + | func (c *testPopupOpenerV8Context) Eval(code string, scriptURL string, _ int32, _, _ unsafe.Pointer) int32 { | |||
| 79 | + | c.evalCode = code | |||
| 80 | + | c.evalURL = scriptURL | |||
| 81 | + | return 1 | |||
| 82 | + | } | |||
| 83 | + | func (c *testPopupOpenerV8Context) RawPointer() unsafe.Pointer { return nil } | |||
| 84 | + | func (c *testPopupOpenerV8Context) Release() {} | |||
| 85 | + | ||||
| 86 | + | func TestDecodePopupOpenerRenderMetadata(t *testing.T) { | |||
| 87 | + | metadata, ok := decodePopupOpenerRenderMetadata(testPopupOpenerDictionaryValue{ | |||
| 88 | + | strings: map[string]string{ | |||
| 89 | + | popupOpenerExtraInfoParentURIKey: "https://example.com/login", | |||
| 90 | + | popupOpenerExtraInfoBridgeNonceKey: "bridge-nonce", | |||
| 91 | + | }, | |||
| 92 | + | bools: map[string]int32{ | |||
| 93 | + | popupOpenerExtraInfoEnabledKey: 1, | |||
| 94 | + | }, | |||
| 95 | + | }) | |||
| 96 | + | ||||
| 97 | + | require.True(t, ok) | |||
| 98 | + | require.Equal(t, "https://example.com/login", metadata.ParentURI) | |||
| 99 | + | require.Equal(t, "bridge-nonce", metadata.BridgeNonce) | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | func TestPopupOpenerRenderProcessHandler_OnContextCreatedInstallsBridgeForConfiguredMainFrame(t *testing.T) { | |||
| 103 | + | h := newPopupOpenerRenderProcessHandler() | |||
| 104 | + | browser := cefmocks.NewMockBrowser(t) | |||
| 105 | + | browser.EXPECT().GetIdentifier().Return(int32(42)).Twice() | |||
| 106 | + | ctx := &testPopupOpenerV8Context{valid: true} | |||
| 107 | + | ||||
| 108 | + | h.OnBrowserCreated(browser, testPopupOpenerDictionaryValue{ | |||
| 109 | + | strings: map[string]string{ | |||
| 110 | + | popupOpenerExtraInfoParentURIKey: "https://example.com/login", | |||
| 111 | + | popupOpenerExtraInfoBridgeNonceKey: "bridge-nonce", | |||
| 112 | + | }, | |||
| 113 | + | bools: map[string]int32{ | |||
| 114 | + | popupOpenerExtraInfoEnabledKey: 1, | |||
| 115 | + | }, | |||
| 116 | + | }) | |||
| 117 | + | h.OnContextCreated(browser, stubFrame{main: true, url: "https://example.com/popup"}, ctx) | |||
| 118 | + | ||||
| 119 | + | require.Equal(t, 1, ctx.enterCalls) | |||
| 120 | + | require.Equal(t, 1, ctx.exitCalls) | |||
| 121 | + | require.Contains(t, ctx.evalCode, "popup-opener-navigate") | |||
| 122 | + | require.Contains(t, ctx.evalCode, "popup-opener-post-message") | |||
| 123 | + | require.Contains(t, ctx.evalCode, "https://example.com/login") | |||
| 124 | + | require.Contains(t, ctx.evalCode, "bridge-nonce") | |||
| 125 | + | require.Equal(t, popupOpenerRenderScriptURL, ctx.evalURL) | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | func TestPopupOpenerRenderProcessHandler_OnContextCreatedSkipsWithoutMetadata(t *testing.T) { | |||
| 129 | + | h := newPopupOpenerRenderProcessHandler() | |||
| 130 | + | browser := cefmocks.NewMockBrowser(t) | |||
| 131 | + | browser.EXPECT().GetIdentifier().Return(int32(7)).Once() | |||
| 132 | + | ctx := &testPopupOpenerV8Context{valid: true} | |||
| 133 | + | ||||
| 134 | + | h.OnContextCreated(browser, stubFrame{main: true, url: "https://example.com/popup"}, ctx) | |||
| 135 | + | ||||
| 136 | + | require.Zero(t, ctx.enterCalls) | |||
| 137 | + | require.Empty(t, ctx.evalCode) | |||
| 138 | + | } | |||
| 139 | + | ||||
| 140 | + | func TestEnablePopupOpenerBridge_SyncsPendingCreateExtraInfo(t *testing.T) { | |||
| 141 | + | oldBuilder := popupOpenerRenderExtraInfoBuilder | |||
| 142 | + | popupOpenerRenderExtraInfoBuilder = func(parentURI, bridgeNonce string) purecef.DictionaryValue { | |||
| 143 | + | require.Equal(t, "https://example.com/login", parentURI) | |||
| 144 | + | require.NotEmpty(t, bridgeNonce) | |||
| 145 | + | return testPopupOpenerDictionaryValue{strings: map[string]string{"ok": "1"}} | |||
| 146 | + | } | |||
| 147 | + | t.Cleanup(func() { | |||
| 148 | + | popupOpenerRenderExtraInfoBuilder = oldBuilder | |||
| 149 | + | }) | |||
| 150 | + | ||||
| 151 | + | parent := &WebView{} | |||
| 152 | + | parent.updateURI("https://example.com/login") | |||
| 153 | + | windowInfo := purecef.NewWindowInfo() | |||
| 154 | + | wv := &WebView{pendingCreate: &pendingBrowserCreate{windowInfo: &windowInfo}} | |||
| 155 | + | ||||
| 156 | + | wv.EnablePopupOpenerBridge(parent, false) | |||
| 157 | + | ||||
| 158 | + | require.NotNil(t, wv.pendingCreate.extraInfo) | |||
| 159 | + | require.NotEmpty(t, wv.bridgeNonce) | |||
| 160 | + | } | |||
M internal/infrastructure/cef/webview.go +2 -0
| @@ -180,6 +180,7 @@ type pendingBrowserCreate struct { | |||
| 180 | 180 | windowInfo *purecef.WindowInfo | |
| 181 | 181 | client purecef.RawClient | |
| 182 | 182 | settings *purecef.BrowserSettings | |
| 183 | + | extraInfo purecef.DictionaryValue | |
| 183 | 184 | } | |
| 184 | 185 | ||
| 185 | 186 | type cefTaskFunc func() | |
| @@ -727,6 +728,7 @@ func (wv *WebView) preparePopupShellDirectBrowserCreation() bool { | |||
| 727 | 728 | wv.popupOpenerBridgeParent = nil | |
| 728 | 729 | wv.popupOpenerBridgeParentURI = "" | |
| 729 | 730 | } | |
| 731 | + | wv.syncPopupOpenerBridgeExtraInfoLocked() | |
| 730 | 732 | wv.mu.Unlock() | |
| 731 | 733 | if parent != nil && popupID != 0 { | |
| 732 | 734 | parent.clearPendingNativePopup(popupID, wv) | |
| @@ -180,6 +180,7 @@ type pendingBrowserCreate struct { | |||||
| 180 | windowInfo *purecef.WindowInfo | 180 | windowInfo *purecef.WindowInfo | ||
| 181 | client purecef.RawClient | 181 | client purecef.RawClient | ||
| 182 | settings *purecef.BrowserSettings | 182 | settings *purecef.BrowserSettings | ||
| 183 | + | extraInfo purecef.DictionaryValue | |||
| 183 | } | 184 | } | ||
| 184 | 185 | ||||
| 185 | type cefTaskFunc func() | 186 | type cefTaskFunc func() | ||
| @@ -727,6 +728,7 @@ func (wv *WebView) preparePopupShellDirectBrowserCreation() bool { | |||||
| 727 | wv.popupOpenerBridgeParent = nil | 728 | wv.popupOpenerBridgeParent = nil | ||
| 728 | wv.popupOpenerBridgeParentURI = "" | 729 | wv.popupOpenerBridgeParentURI = "" | ||
| 729 | } | 730 | } | ||
| 731 | + | wv.syncPopupOpenerBridgeExtraInfoLocked() | |||
| 730 | wv.mu.Unlock() | 732 | wv.mu.Unlock() | ||
| 731 | if parent != nil && popupID != 0 { | 733 | if parent != nil && popupID != 0 { | ||
| 732 | parent.clearPendingNativePopup(popupID, wv) | 734 | parent.clearPendingNativePopup(popupID, wv) | ||
M internal/infrastructure/cef/webview_handlers.go +11 -1
| @@ -601,8 +601,18 @@ func (h *handlerSet) OnLoadStart(_ purecef.Browser, frame purecef.Frame, _ purec | |||
| 601 | 601 | if frame == nil || !frame.IsMain() { | |
| 602 | 602 | return | |
| 603 | 603 | } | |
| 604 | - | bridgeNonce := h.wv.rotateBridgeNonce() | |
| 604 | + | bridgeNonce := h.wv.ensureBridgeNonce() | |
| 605 | 605 | if openerBridgeScript := h.wv.popupOpenerBridgeScript(bridgeNonce); openerBridgeScript != "" { | |
| 606 | + | if h.wv != nil && h.wv.ctx != nil { | |
| 607 | + | parentURI, active, blocked := h.wv.popupOpenerBridgeState() | |
| 608 | + | logging.FromContext(h.wv.ctx).Debug(). | |
| 609 | + | Uint64("webview_id", uint64(h.wv.id)). | |
| 610 | + | Str("url", logging.TruncateURL(frame.GetURL(), logging.PermissionLogURLMaxLen)). | |
| 611 | + | Str("parent_uri", logging.TruncateURL(parentURI, logging.PermissionLogURLMaxLen)). | |
| 612 | + | Bool("bridge_active", active). | |
| 613 | + | Bool("bridge_blocked", blocked). | |
| 614 | + | Msg("cef: injecting popup opener bridge") | |
| 615 | + | } | |
| 606 | 616 | frame.ExecuteJavaScript(openerBridgeScript, frame.GetURL(), 0) | |
| 607 | 617 | } | |
| 608 | 618 | if h.wv != nil && h.wv.ctx != nil { | |
| @@ -601,8 +601,18 @@ func (h *handlerSet) OnLoadStart(_ purecef.Browser, frame purecef.Frame, _ purec | |||||
| 601 | if frame == nil || !frame.IsMain() { | 601 | if frame == nil || !frame.IsMain() { | ||
| 602 | return | 602 | return | ||
| 603 | } | 603 | } | ||
| 604 | - | bridgeNonce := h.wv.rotateBridgeNonce() | 604 | + | bridgeNonce := h.wv.ensureBridgeNonce() |
| 605 | if openerBridgeScript := h.wv.popupOpenerBridgeScript(bridgeNonce); openerBridgeScript != "" { | 605 | if openerBridgeScript := h.wv.popupOpenerBridgeScript(bridgeNonce); openerBridgeScript != "" { | ||
| 606 | + | if h.wv != nil && h.wv.ctx != nil { | |||
| 607 | + | parentURI, active, blocked := h.wv.popupOpenerBridgeState() | |||
| 608 | + | logging.FromContext(h.wv.ctx).Debug(). | |||
| 609 | + | Uint64("webview_id", uint64(h.wv.id)). | |||
| 610 | + | Str("url", logging.TruncateURL(frame.GetURL(), logging.PermissionLogURLMaxLen)). | |||
| 611 | + | Str("parent_uri", logging.TruncateURL(parentURI, logging.PermissionLogURLMaxLen)). | |||
| 612 | + | Bool("bridge_active", active). | |||
| 613 | + | Bool("bridge_blocked", blocked). | |||
| 614 | + | Msg("cef: injecting popup opener bridge") | |||
| 615 | + | } | |||
| 606 | frame.ExecuteJavaScript(openerBridgeScript, frame.GetURL(), 0) | 616 | frame.ExecuteJavaScript(openerBridgeScript, frame.GetURL(), 0) | ||
| 607 | } | 617 | } | ||
| 608 | if h.wv != nil && h.wv.ctx != nil { | 618 | if h.wv != nil && h.wv.ctx != nil { | ||
M internal/ui/coordinator/content/oauth.go +5 -0
| @@ -317,6 +317,11 @@ func (c *Coordinator) setupOAuthAutoClose( | |||
| 317 | 317 | // Start safety timer immediately. | |
| 318 | 318 | startSafetyTimer() | |
| 319 | 319 | deferCloseOnNavigation := popupUsesSyntheticOpenerSignals(wv) | |
| 320 | + | log.Debug(). | |
| 321 | + | Str("pane", string(paneID)). | |
| 322 | + | Uint64("popup_id", uint64(popupID)). | |
| 323 | + | Bool("synthetic_opener_active", deferCloseOnNavigation). | |
| 324 | + | Msg("oauth auto-close configured") | |
| 320 | 325 | ||
| 321 | 326 | // Register navigation callback to check for OAuth callbacks on URI changes and committed loads. | |
| 322 | 327 | oauthWV.AddNavigationCallback(func(uri string) { | |
| @@ -317,6 +317,11 @@ func (c *Coordinator) setupOAuthAutoClose( | |||||
| 317 | // Start safety timer immediately. | 317 | // Start safety timer immediately. | ||
| 318 | startSafetyTimer() | 318 | startSafetyTimer() | ||
| 319 | deferCloseOnNavigation := popupUsesSyntheticOpenerSignals(wv) | 319 | deferCloseOnNavigation := popupUsesSyntheticOpenerSignals(wv) | ||
| 320 | + | log.Debug(). | |||
| 321 | + | Str("pane", string(paneID)). | |||
| 322 | + | Uint64("popup_id", uint64(popupID)). | |||
| 323 | + | Bool("synthetic_opener_active", deferCloseOnNavigation). | |||
| 324 | + | Msg("oauth auto-close configured") | |||
| 320 | 325 | ||||
| 321 | // Register navigation callback to check for OAuth callbacks on URI changes and committed loads. | 326 | // Register navigation callback to check for OAuth callbacks on URI changes and committed loads. | ||
| 322 | oauthWV.AddNavigationCallback(func(uri string) { | 327 | oauthWV.AddNavigationCallback(func(uri string) { | ||
M internal/ui/coordinator/content/popup_manager.go +15 -1
| @@ -671,7 +671,21 @@ func (pm *popupManager) handlePopupReadyToShow(ctx context.Context, popupID port | |||
| 671 | 671 | ||
| 672 | 672 | func (pm *popupManager) handlePopupClose(ctx context.Context, hooks popupCoordinatorHooks, popupID port.WebViewID) { | |
| 673 | 673 | log := logging.FromContext(ctx) | |
| 674 | - | log.Debug().Uint64("popup_id", uint64(popupID)).Msg("popup close signal received") | |
| 674 | + | fields := log.Debug().Uint64("popup_id", uint64(popupID)) | |
| 675 | + | if hooks.findPaneByWebViewID != nil && hooks.getWebView != nil { | |
| 676 | + | if paneID, ok := hooks.findPaneByWebViewID(popupID); ok && paneID != "" { | |
| 677 | + | fields = fields.Str("pane_id", string(paneID)) | |
| 678 | + | if wv := hooks.getWebView(paneID); wv != nil { | |
| 679 | + | fields = fields. | |
| 680 | + | Str("current_uri", logging.TruncateURL(wv.URI(), logURLMaxLen)). | |
| 681 | + | Bool("is_loading", wv.IsLoading()) | |
| 682 | + | if opener, ok := wv.(port.PopupOpenerCapable); ok { | |
| 683 | + | fields = fields.Bool("synthetic_opener_active", opener.HasActivePopupOpenerBridge()) | |
| 684 | + | } | |
| 685 | + | } | |
| 686 | + | } | |
| 687 | + | } | |
| 688 | + | fields.Msg("popup close signal received") | |
| 675 | 689 | ||
| 676 | 690 | pending, wasPending := pm.takePendingPopup(popupID) | |
| 677 | 691 | if wasPending && pending != nil { | |
| @@ -671,7 +671,21 @@ func (pm *popupManager) handlePopupReadyToShow(ctx context.Context, popupID port | |||||
| 671 | 671 | ||||
| 672 | func (pm *popupManager) handlePopupClose(ctx context.Context, hooks popupCoordinatorHooks, popupID port.WebViewID) { | 672 | func (pm *popupManager) handlePopupClose(ctx context.Context, hooks popupCoordinatorHooks, popupID port.WebViewID) { | ||
| 673 | log := logging.FromContext(ctx) | 673 | log := logging.FromContext(ctx) | ||
| 674 | - | log.Debug().Uint64("popup_id", uint64(popupID)).Msg("popup close signal received") | 674 | + | fields := log.Debug().Uint64("popup_id", uint64(popupID)) |
| 675 | + | if hooks.findPaneByWebViewID != nil && hooks.getWebView != nil { | |||
| 676 | + | if paneID, ok := hooks.findPaneByWebViewID(popupID); ok && paneID != "" { | |||
| 677 | + | fields = fields.Str("pane_id", string(paneID)) | |||
| 678 | + | if wv := hooks.getWebView(paneID); wv != nil { | |||
| 679 | + | fields = fields. | |||
| 680 | + | Str("current_uri", logging.TruncateURL(wv.URI(), logURLMaxLen)). | |||
| 681 | + | Bool("is_loading", wv.IsLoading()) | |||
| 682 | + | if opener, ok := wv.(port.PopupOpenerCapable); ok { | |||
| 683 | + | fields = fields.Bool("synthetic_opener_active", opener.HasActivePopupOpenerBridge()) | |||
| 684 | + | } | |||
| 685 | + | } | |||
| 686 | + | } | |||
| 687 | + | } | |||
| 688 | + | fields.Msg("popup close signal received") | |||
| 675 | 689 | ||||
| 676 | pending, wasPending := pm.takePendingPopup(popupID) | 690 | pending, wasPending := pm.takePendingPopup(popupID) | ||
| 677 | if wasPending && pending != nil { | 691 | if wasPending && pending != nil { | ||