1package cef
2
3import (
4 "context"
5 "fmt"
6 neturl "net/url"
7 "strings"
8
9 "github.com/bnema/dumber/internal/application/port"
10 "github.com/bnema/dumber/internal/infrastructure/webutil"
11 "github.com/bnema/dumber/internal/logging"
12)
13
14type popupOpenerPostMessagePayload struct {
15 Data string `json:"data"`
16 DataKind string `json:"data_kind"`
17 TargetOrigin string `json:"target_origin"`
18 SourceOrigin string `json:"source_origin"`
19 SourceHref string `json:"source_href"`
20}
21
22type popupOpenerNavigatePayload struct {
23 URL string `json:"url"`
24}
25
26const popupOpenerHTTPScheme = "http"
27
28func (wv *WebView) setPopupNoJavaScriptAccess(noJavaScriptAccess bool) {
29 if wv == nil {
30 return
31 }
32 wv.mu.Lock()
33 wv.popupNoJavaScriptAccess = noJavaScriptAccess
34 wv.mu.Unlock()
35}
36
37// AddOpenerMessageCallback implements port.PopupOpenerCapable.
38func (wv *WebView) AddOpenerMessageCallback(fn func()) {
39 if wv == nil || fn == nil {
40 return
41 }
42 wv.mu.Lock()
43 wv.openerMessageCallbacks = append(wv.openerMessageCallbacks, fn)
44 wv.mu.Unlock()
45}
46
47// AddOpenerNavigationCallback implements port.PopupOpenerCapable.
48func (wv *WebView) AddOpenerNavigationCallback(fn func(uri string)) {
49 if wv == nil || fn == nil {
50 return
51 }
52 wv.mu.Lock()
53 wv.openerNavigationCallbacks = append(wv.openerNavigationCallbacks, fn)
54 wv.mu.Unlock()
55}
56
57func (wv *WebView) runOpenerMessageCallbacks() {
58 if wv == nil {
59 return
60 }
61 wv.mu.RLock()
62 callbacks := append([]func(){}, wv.openerMessageCallbacks...)
63 wv.mu.RUnlock()
64 if len(callbacks) == 0 {
65 return
66 }
67 wv.runOnGTK(func() {
68 for _, fn := range callbacks {
69 if fn != nil {
70 fn()
71 }
72 }
73 })
74}
75
76func (wv *WebView) runOpenerNavigationCallbacks(uri string) {
77 if wv == nil {
78 return
79 }
80 wv.mu.RLock()
81 callbacks := append([]func(string){}, wv.openerNavigationCallbacks...)
82 wv.mu.RUnlock()
83 if len(callbacks) == 0 {
84 return
85 }
86 wv.runOnGTK(func() {
87 for _, fn := range callbacks {
88 if fn != nil {
89 fn(uri)
90 }
91 }
92 })
93}
94
95// HasActivePopupOpenerBridge implements port.PopupOpenerCapable.
96func (wv *WebView) HasActivePopupOpenerBridge() bool {
97 _, active, _ := wv.popupOpenerBridgeState()
98 return active
99}
100
101func (wv *WebView) popupOpenerBridgeState() (parentURI string, active, blocked bool) {
102 if wv == nil {
103 return "", false, false
104 }
105 wv.mu.RLock()
106 defer wv.mu.RUnlock()
107 blocked = wv.popupNoJavaScriptAccess
108 active = !blocked && wv.popupOpenerBridgeParent != nil
109 parentURI = wv.popupOpenerBridgeParentURI
110 return parentURI, active, blocked
111}
112
113func (wv *WebView) ensureBridgeNonceLocked() string {
114 if wv == nil {
115 return ""
116 }
117 if wv.bridgeNonce != "" {
118 return wv.bridgeNonce
119 }
120 nonce := newBridgeNonce()
121 if nonce == "" {
122 return ""
123 }
124 wv.bridgeNonce = nonce
125 return nonce
126}
127
128func (wv *WebView) syncPopupOpenerBridgeExtraInfoLocked() {
129 if wv == nil || wv.pendingCreate == nil || wv.pendingCreate.windowInfo == nil {
130 return
131 }
132 if wv.popupNoJavaScriptAccess || wv.popupOpenerBridgeParent == nil || wv.popupOpenerBridgeParentURI == "" {
133 wv.pendingCreate.extraInfo = nil
134 return
135 }
136 bridgeNonce := wv.ensureBridgeNonceLocked()
137 if bridgeNonce == "" {
138 wv.pendingCreate.extraInfo = nil
139 return
140 }
141 wv.pendingCreate.extraInfo = popupOpenerRenderExtraInfoBuilder(wv.popupOpenerBridgeParentURI, bridgeNonce)
142}
143
144// EnablePopupOpenerBridge implements port.PopupOpenerCapable.
145func (wv *WebView) EnablePopupOpenerBridge(parent port.WebView, noJavaScriptAccess bool) {
146 if wv == nil {
147 return
148 }
149 wv.mu.Lock()
150 defer wv.mu.Unlock()
151 wv.popupNoJavaScriptAccess = noJavaScriptAccess
152 if noJavaScriptAccess {
153 wv.popupOpenerBridgeParent = nil
154 wv.popupOpenerBridgeParentURI = ""
155 wv.syncPopupOpenerBridgeExtraInfoLocked()
156 if wv.ctx != nil {
157 logging.FromContext(wv.ctx).Debug().
158 Uint64("webview_id", uint64(wv.id)).
159 Bool("no_javascript_access", true).
160 Msg("cef: popup opener bridge disabled")
161 }
162 return
163 }
164 parentWV, ok := parent.(*WebView)
165 if !ok || parentWV == nil {
166 wv.popupOpenerBridgeParent = nil
167 wv.popupOpenerBridgeParentURI = ""
168 wv.syncPopupOpenerBridgeExtraInfoLocked()
169 if wv.ctx != nil {
170 logging.FromContext(wv.ctx).Debug().
171 Uint64("webview_id", uint64(wv.id)).
172 Msg("cef: popup opener bridge unavailable: parent missing")
173 }
174 return
175 }
176 wv.popupOpenerBridgeParent = parentWV
177 wv.popupOpenerBridgeParentURI = parentWV.URI()
178 wv.syncPopupOpenerBridgeExtraInfoLocked()
179 if wv.ctx != nil {
180 logging.FromContext(wv.ctx).Debug().
181 Uint64("webview_id", uint64(wv.id)).
182 Uint64("parent_webview_id", uint64(parentWV.id)).
183 Str("parent_uri", logging.TruncateURL(wv.popupOpenerBridgeParentURI, logging.PermissionLogURLMaxLen)).
184 Msg("cef: popup opener bridge enabled")
185 }
186}
187
188func (wv *WebView) popupOpenerBridgeScript(bridgeNonce string) string {
189 if wv == nil || bridgeNonce == "" {
190 return ""
191 }
192
193 wv.mu.RLock()
194 parentURI := wv.popupOpenerBridgeParentURI
195 parent := wv.popupOpenerBridgeParent
196 noJavaScriptAccess := wv.popupNoJavaScriptAccess
197 blocked := noJavaScriptAccess || parent == nil
198 wv.mu.RUnlock()
199 if blocked {
200 if wv.ctx != nil {
201 logging.FromContext(wv.ctx).Debug().
202 Uint64("webview_id", uint64(wv.id)).
203 Bool("blocked", true).
204 Bool("has_parent", parent != nil).
205 Bool("no_javascript_access", noJavaScriptAccess).
206 Msg("cef: popup opener bridge script skipped")
207 }
208 return ""
209 }
210 if wv.ctx != nil {
211 logging.FromContext(wv.ctx).Debug().
212 Uint64("webview_id", uint64(wv.id)).
213 Str("parent_uri", logging.TruncateURL(parentURI, logging.PermissionLogURLMaxLen)).
214 Msg("cef: popup opener bridge script prepared")
215 }
216
217 return buildPopupOpenerBridgeJS(bridgeNonce, parentURI)
218}
219
220func (wv *WebView) handlePopupOpenerNavigate(targetURL string) {
221 if wv == nil {
222 return
223 }
224
225 trimmedURL := strings.TrimSpace(targetURL)
226 if trimmedURL == "" {
227 return
228 }
229
230 wv.mu.RLock()
231 opener := wv.popupOpenerBridgeParent
232 wv.mu.RUnlock()
233 if opener == nil || opener.destroyed.Load() {
234 if wv.ctx != nil {
235 logging.FromContext(wv.ctx).Debug().
236 Uint64("webview_id", uint64(wv.id)).
237 Str("target_url", logging.TruncateURL(trimmedURL, logging.PermissionLogURLMaxLen)).
238 Msg("cef: popup opener navigate ignored: opener unavailable")
239 }
240 return
241 }
242
243 resolvedURL := resolvePopupOpenerNavigationTarget(trimmedURL, opener.URI())
244 if resolvedURL == "" {
245 if wv.ctx != nil {
246 logging.FromContext(wv.ctx).Warn().
247 Uint64("webview_id", uint64(wv.id)).
248 Str("target_url", logging.TruncateURL(trimmedURL, logging.PermissionLogURLMaxLen)).
249 Msg("cef: popup opener navigate rejected")
250 }
251 return
252 }
253 if wv.ctx != nil {
254 logging.FromContext(wv.ctx).Debug().
255 Uint64("webview_id", uint64(wv.id)).
256 Str("target_url", logging.TruncateURL(trimmedURL, logging.PermissionLogURLMaxLen)).
257 Str("resolved_url", logging.TruncateURL(resolvedURL, logging.PermissionLogURLMaxLen)).
258 Msg("cef: popup opener navigate received")
259 }
260
261 if err := opener.LoadURI(context.Background(), resolvedURL); err != nil && opener.ctx != nil {
262 logging.FromContext(opener.ctx).Warn().
263 Err(err).
264 Str("uri", logging.TruncateURL(resolvedURL, logging.PermissionLogURLMaxLen)).
265 Msg("cef: failed to navigate synthetic popup opener")
266 }
267
268 wv.mu.Lock()
269 wv.popupOpenerBridgeParentURI = resolvedURL
270 wv.mu.Unlock()
271 wv.runOpenerNavigationCallbacks(resolvedURL)
272}
273
274func resolvePopupOpenerNavigationTarget(rawTarget, openerURI string) string {
275 trimmedTarget := strings.TrimSpace(rawTarget)
276 if trimmedTarget == "" {
277 return ""
278 }
279 parsedTarget, err := neturl.Parse(trimmedTarget)
280 if err == nil && parsedTarget.IsAbs() {
281 return sanitizePopupOpenerNavigationTarget(parsedTarget)
282 }
283 if strings.HasPrefix(trimmedTarget, "//") {
284 base, baseErr := neturl.Parse(strings.TrimSpace(openerURI))
285 if baseErr == nil && base != nil && base.Scheme != "" {
286 parsedTarget.Scheme = base.Scheme
287 return sanitizePopupOpenerNavigationTarget(parsedTarget)
288 }
289 return ""
290 }
291 base, err := neturl.Parse(strings.TrimSpace(openerURI))
292 if err != nil || base == nil || base.Scheme == "" || base.Host == "" {
293 return ""
294 }
295 ref, err := neturl.Parse(trimmedTarget)
296 if err != nil {
297 return ""
298 }
299 return sanitizePopupOpenerNavigationTarget(base.ResolveReference(ref))
300}
301
302func sanitizePopupOpenerNavigationTarget(target *neturl.URL) string {
303 if target == nil {
304 return ""
305 }
306 switch scheme := strings.ToLower(strings.TrimSpace(target.Scheme)); scheme {
307 case popupOpenerHTTPScheme, actualInternalScheme:
308 return target.String()
309 default:
310 return ""
311 }
312}
313
314func (wv *WebView) handlePopupOpenerPostMessage(payload popupOpenerPostMessagePayload) {
315 if wv == nil {
316 return
317 }
318
319 wv.mu.RLock()
320 opener := wv.popupOpenerBridgeParent
321 wv.mu.RUnlock()
322 if opener == nil || opener.destroyed.Load() {
323 if wv.ctx != nil {
324 logging.FromContext(wv.ctx).Debug().
325 Uint64("webview_id", uint64(wv.id)).
326 Str("target_origin", payload.TargetOrigin).
327 Msg("cef: popup opener postMessage ignored: opener unavailable")
328 }
329 return
330 }
331 if !targetOriginMatchesPopupOpener(payload.TargetOrigin, opener.URI()) {
332 if wv.ctx != nil {
333 logging.FromContext(wv.ctx).Warn().
334 Uint64("webview_id", uint64(wv.id)).
335 Str("target_origin", payload.TargetOrigin).
336 Str("opener_uri", logging.TruncateURL(opener.URI(), logging.PermissionLogURLMaxLen)).
337 Msg("cef: popup opener postMessage rejected")
338 }
339 return
340 }
341 sourceOrigin, sourceHref := popupOpenerSourceMetadata(wv)
342 if wv.ctx != nil {
343 logging.FromContext(wv.ctx).Debug().
344 Uint64("webview_id", uint64(wv.id)).
345 Str("target_origin", payload.TargetOrigin).
346 Str("source_origin", sourceOrigin).
347 Str("source_href", logging.TruncateURL(sourceHref, logging.PermissionLogURLMaxLen)).
348 Msg("cef: popup opener postMessage received")
349 }
350
351 var dataExpr string
352 switch strings.ToLower(strings.TrimSpace(payload.DataKind)) {
353 case "json":
354 dataExpr = fmt.Sprintf("JSON.parse('%s')", webutil.EscapeForJSString(payload.Data))
355 case "string":
356 dataExpr = fmt.Sprintf("'%s'", webutil.EscapeForJSString(payload.Data))
357 default:
358 dataExpr = fmt.Sprintf("'%s'", webutil.EscapeForJSString(payload.Data))
359 }
360
361 script := fmt.Sprintf(`(function() {
362 try {
363 var data;
364 try {
365 data = %s;
366 } catch (_) {
367 data = undefined;
368 }
369 var event = new MessageEvent('message', {
370 data: data,
371 origin: '%s',
372 source: null
373 });
374 var sourceProxy = {
375 closed: false,
376 close: function() { return undefined; },
377 focus: function() { return undefined; },
378 postMessage: function() { return undefined; },
379 location: {
380 href: '%s',
381 toString: function() { return this.href; }
382 }
383 };
384 try {
385 Object.defineProperty(event, 'source', {
386 configurable: true,
387 enumerable: true,
388 value: sourceProxy
389 });
390 } catch (_) {}
391 window.dispatchEvent(event);
392 } catch (_) {}
393})();`, dataExpr, webutil.EscapeForJSString(sourceOrigin), webutil.EscapeForJSString(sourceHref))
394 opener.RunJavaScript(context.Background(), script)
395 wv.runOpenerMessageCallbacks()
396}
397
398func targetOriginMatchesPopupOpener(targetOrigin, openerURI string) bool {
399 trimmedTarget := strings.TrimSpace(targetOrigin)
400 if trimmedTarget == "" || trimmedTarget == "*" {
401 return true
402 }
403 targetCanonical := canonicalOrigin(trimmedTarget)
404 return targetCanonical != "" && targetCanonical == canonicalOrigin(openerURI)
405}
406
407func popupOpenerSourceMetadata(wv *WebView) (origin, href string) {
408 if wv == nil {
409 return "", ""
410 }
411 href = strings.TrimSpace(wv.URI())
412 if href == "" {
413 href = strings.TrimSpace(wv.pendingNavigationURI())
414 }
415 if href == "" {
416 return "", ""
417 }
418 return originFromURL(href), href
419}
420
421func originFromURL(rawURL string) string {
422 canonicalURL := toActualInternalURL(strings.TrimSpace(rawURL))
423 parsed, err := neturl.Parse(canonicalURL)
424 if err != nil || parsed.Scheme == "" || parsed.Host == "" {
425 return ""
426 }
427 return parsed.Scheme + "://" + parsed.Host
428}
429
430func canonicalOrigin(rawURL string) string {
431 canonicalURL := toActualInternalURL(strings.TrimSpace(rawURL))
432 parsed, err := neturl.Parse(canonicalURL)
433 if err != nil || parsed.Scheme == "" || parsed.Host == "" {
434 return ""
435 }
436 scheme := strings.ToLower(parsed.Scheme)
437 host := strings.ToLower(parsed.Hostname())
438 if host == "" {
439 return ""
440 }
441 portStr := parsed.Port()
442 if portStr == "" {
443 switch scheme {
444 case popupOpenerHTTPScheme:
445 portStr = "80"
446 case actualInternalScheme:
447 portStr = "443"
448 }
449 }
450 if portStr == "" {
451 return scheme + "://" + host
452 }
453 return scheme + "://" + host + ":" + portStr
454}