popup_opener_bridge.go RawHistoryFolder

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}

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