detached 71feac4

cef: install popup opener bridge in renderer

commit
71feac4d83efe4a0d185dfe46ce9b7e231f7fbf1
author
brice · · 2026-04-23 04:32 UTC
parent
1df2f44
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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) {
4545 type dumberApp struct {
4646 engine *Engine
4747 bph purecef.BrowserProcessHandler
48+ rph purecef.RenderProcessHandler
4849 }
4950
5051 // newDumberApp creates an App whose BrowserProcessHandler handles context
@@ -53,15 +54,16 @@ type dumberApp struct {
5354 func newDumberApp(engine *Engine) purecef.App {
5455 app := &dumberApp{engine: engine}
5556 app.bph = &dumberBPH{engine: engine}
57+ app.rph = newPopupOpenerRenderProcessHandler()
5658 return app
5759 }
5860
5961 // 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.
6365 func NewSubprocessApp() purecef.App {
64- return &subprocessApp{}
66+ return &subprocessApp{rph: newPopupOpenerRenderProcessHandler()}
6567 }
6668
6769 // maxCmdLineLogLen limits logged command line length to avoid leaking sensitive paths.
@@ -91,9 +93,9 @@ func (a *dumberApp) OnRegisterCustomSchemes(registrar purecef.SchemeRegistrar) {
9193 }
9294 func (a *dumberApp) GetResourceBundleHandler() purecef.ResourceBundleHandler { return nil }
9395 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 }
9597
96-type subprocessApp struct{}
98+type subprocessApp struct{ rph purecef.RenderProcessHandler }
9799
98100 func (a *subprocessApp) OnBeforeCommandLineProcessing(_ string, commandLine purecef.CommandLine) {
99101 configureCommandLine(commandLine)
@@ -103,7 +105,7 @@ func (a *subprocessApp) OnRegisterCustomSchemes(registrar purecef.SchemeRegistra
103105 }
104106 func (a *subprocessApp) GetResourceBundleHandler() purecef.ResourceBundleHandler { return nil }
105107 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 }
107109
108110 // dumberBPH implements purecef.BrowserProcessHandler for context initialization,
109111 // 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 *Engine46 engine *Engine
47 bph purecef.BrowserProcessHandler47 bph purecef.BrowserProcessHandler
48+ rph purecef.RenderProcessHandler
48 }49 }
49 50
50 // newDumberApp creates an App whose BrowserProcessHandler handles context51 // 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 app58 return app
57 }59 }
58 60
59 // NewSubprocessApp returns a lightweight raw App implementation for CEF61 // NewSubprocessApp returns a lightweight raw App implementation for CEF
60-// subprocesses re-executed from the main dumber binary. Keep subprocesses free62+// subprocesses re-executed from the main dumber binary. Keep the legacy
61-// of custom render handlers for now: the renderer bridge introduced a startup63+// 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
247247 pc.client,
248248 initialURL,
249249 pc.settings,
250- nil, // extraInfo
250+ pc.extraInfo,
251251 nil, // requestContext
252252 )
253253 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, // extraInfo250+ pc.extraInfo,
251 nil, // requestContext251 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 {
100100 return !wv.popupNoJavaScriptAccess && wv.popupOpenerBridgeParent != nil
101101 }
102102
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+
103146 // EnablePopupOpenerBridge implements port.PopupOpenerCapable.
104147 func (wv *WebView) EnablePopupOpenerBridge(parent port.WebView, noJavaScriptAccess bool) {
105148 if wv == nil {
@@ -111,16 +154,37 @@ func (wv *WebView) EnablePopupOpenerBridge(parent port.WebView, noJavaScriptAcce
111154 if noJavaScriptAccess {
112155 wv.popupOpenerBridgeParent = nil
113156 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+ }
114164 return
115165 }
116166 parentWV, ok := parent.(*WebView)
117167 if !ok || parentWV == nil {
118168 wv.popupOpenerBridgeParent = nil
119169 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+ }
120176 return
121177 }
122178 wv.popupOpenerBridgeParent = parentWV
123179 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+ }
124188 }
125189
126190 func (wv *WebView) popupOpenerBridgeScript(bridgeNonce string) string {
@@ -131,11 +195,26 @@ func (wv *WebView) popupOpenerBridgeScript(bridgeNonce string) string {
131195 wv.mu.RLock()
132196 parentURI := wv.popupOpenerBridgeParentURI
133197 parent := wv.popupOpenerBridgeParent
134- blocked := wv.popupNoJavaScriptAccess || parent == nil
198+ noJavaScriptAccess := wv.popupNoJavaScriptAccess
199+ blocked := noJavaScriptAccess || parent == nil
135200 wv.mu.RUnlock()
136201 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+ }
137210 return ""
138211 }
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+ }
139218
140219 return buildPopupOpenerBridgeJS(bridgeNonce, parentURI)
141220 }
@@ -154,13 +233,32 @@ func (wv *WebView) handlePopupOpenerNavigate(targetURL string) {
154233 opener := wv.popupOpenerBridgeParent
155234 wv.mu.RUnlock()
156235 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+ }
157242 return
158243 }
159244
160245 resolvedURL := resolvePopupOpenerNavigationTarget(trimmedURL, opener.URI())
161246 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+ }
162253 return
163254 }
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+ }
164262
165263 if err := opener.LoadURI(context.Background(), resolvedURL); err != nil && opener.ctx != nil {
166264 logging.FromContext(opener.ctx).Warn().
@@ -224,11 +322,32 @@ func (wv *WebView) handlePopupOpenerPostMessage(payload popupOpenerPostMessagePa
224322 opener := wv.popupOpenerBridgeParent
225323 wv.mu.RUnlock()
226324 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+ }
227331 return
228332 }
229333 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+ }
230341 return
231342 }
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+ }
232351
233352 sourceOrigin := popupSourceOrigin(payload)
234353 dataExpr := "undefined"
@@ -100,6 +100,49 @@ func (wv *WebView) HasActivePopupOpenerBridge() bool {
100 return !wv.popupNoJavaScriptAccess && wv.popupOpenerBridgeParent != nil100 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 = nil155 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 return164 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 = nil168 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 return176 return
121 }177 }
122 wv.popupOpenerBridgeParent = parentWV178 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.popupOpenerBridgeParentURI196 parentURI := wv.popupOpenerBridgeParentURI
133 parent := wv.popupOpenerBridgeParent197 parent := wv.popupOpenerBridgeParent
134- blocked := wv.popupNoJavaScriptAccess || parent == nil198+ 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.popupOpenerBridgeParent233 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 return242 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 return253 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.popupOpenerBridgeParent322 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 return331 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 return341 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 {
180180 windowInfo *purecef.WindowInfo
181181 client purecef.RawClient
182182 settings *purecef.BrowserSettings
183+ extraInfo purecef.DictionaryValue
183184 }
184185
185186 type cefTaskFunc func()
@@ -727,6 +728,7 @@ func (wv *WebView) preparePopupShellDirectBrowserCreation() bool {
727728 wv.popupOpenerBridgeParent = nil
728729 wv.popupOpenerBridgeParentURI = ""
729730 }
731+ wv.syncPopupOpenerBridgeExtraInfoLocked()
730732 wv.mu.Unlock()
731733 if parent != nil && popupID != 0 {
732734 parent.clearPendingNativePopup(popupID, wv)
@@ -180,6 +180,7 @@ type pendingBrowserCreate struct {
180 windowInfo *purecef.WindowInfo180 windowInfo *purecef.WindowInfo
181 client purecef.RawClient181 client purecef.RawClient
182 settings *purecef.BrowserSettings182 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 = nil728 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
601601 if frame == nil || !frame.IsMain() {
602602 return
603603 }
604- bridgeNonce := h.wv.rotateBridgeNonce()
604+ bridgeNonce := h.wv.ensureBridgeNonce()
605605 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+ }
606616 frame.ExecuteJavaScript(openerBridgeScript, frame.GetURL(), 0)
607617 }
608618 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 return602 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(
317317 // Start safety timer immediately.
318318 startSafetyTimer()
319319 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")
320325
321326 // Register navigation callback to check for OAuth callbacks on URI changes and committed loads.
322327 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
671671
672672 func (pm *popupManager) handlePopupClose(ctx context.Context, hooks popupCoordinatorHooks, popupID port.WebViewID) {
673673 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")
675689
676690 pending, wasPending := pm.takePendingPopup(popupID)
677691 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 {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close