detached 1455aeb

fix: address all 13 CodeRabbit findings for Phase 2 CEF code

1. messaging.go: Info→Debug for per-message dispatch logging
2. messaging.go: data race on baseCtx — wrap SetBaseContext/HandleMessage with mutex
3. content_injector.go: escape id parameter in generated JS to prevent injection
4. content_injector.go: add U+2028/U+2029 escaping in escapeForJSString
5. content_injector.go: fix message bridge shim — use MessageBridgeJS constant
   with X-Dumber-Body header (was sending raw body, handler expected header)
6. content_injector.go: fix internalSchemePrefix from cef://dumber to dumb://
7. find_controller.go: remove unused lastID field (dead code)
8. find_controller.go: defensive check for negative count before uint cast
9. engine.go: encapsulate SetColorResolver via contentInjector method
10. engine.go: RegisterHandlers stub now logs Warn instead of misleading Info
11. engine_init.go: check RegisterSchemeHandlerFactory result, log error on failure
12. engine_init.go: document os.Args thread-safety assumption
13. scheme_handler.go: use assetsSet bool instead of embed.FS zero-value comparison,
    HTML-escape error messages, handle json.Marshal errors
commit
1455aeba2cb83e6fe61d2a7c6a41e6ca4c16beab
author
brice · · 2026-03-21 21:34 UTC
committer
brice · · 2026-03-28 04:38 UTC
parent
7b06939
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +46 -37
M internal/infrastructure/cef/content_injector.go +15 -19
@@ -15,7 +15,7 @@ var _ port.ContentInjector = (*contentInjector)(nil)
1515
1616 // internalSchemePrefix is used to detect internal pages that receive dark mode
1717 // and message bridge scripts. Must match the engine's InternalSchemePath prefix.
18-const internalSchemePrefix = "cef://dumber"
18+const internalSchemePrefix = "dumb://"
1919
2020 // contentInjector implements port.ContentInjector for the CEF engine.
2121 // It stores CSS strings and injects them into webviews via ExecuteJavaScript.
@@ -29,6 +29,13 @@ type contentInjector struct {
2929 colorResolver port.ColorSchemeResolver
3030 }
3131
32+// setColorResolver updates the color scheme resolver used for dark mode detection.
33+func (ci *contentInjector) setColorResolver(resolver port.ColorSchemeResolver) {
34+ ci.mu.Lock()
35+ defer ci.mu.Unlock()
36+ ci.colorResolver = resolver
37+}
38+
3239 // newContentInjector creates a content injector wired to the given engine.
3340 func newContentInjector(engine *Engine, resolver port.ColorSchemeResolver) *contentInjector {
3441 return &contentInjector{
@@ -125,12 +132,13 @@ func (ci *contentInjector) onLoadEnd(wv *WebView) {
125132
126133 // injectCSS injects a CSS string as a <style> element via JavaScript.
127134 func (ci *contentInjector) injectCSS(wv *WebView, id, css string) {
135+ escapedID := escapeForJSString(id)
128136 escaped := escapeForJSString(css)
129137 script := fmt.Sprintf(`(function(){
130138 var el = document.getElementById('%s');
131139 if (!el) { el = document.createElement('style'); el.id = '%s'; document.head.appendChild(el); }
132140 el.textContent = '%s';
133-})();`, id, id, escaped)
141+})();`, escapedID, escapedID, escaped)
134142
135143 wv.RunJavaScript(context.Background(), script)
136144 }
@@ -191,24 +199,10 @@ func (ci *contentInjector) injectDarkModeScript(wv *WebView, prefersDark bool) {
191199
192200 // injectMessageBridgeShim injects the window.dumber.postMessage JS client shim
193201 // so internal pages can communicate with Go handlers via fetch.
202+// The message body is base64-encoded into the X-Dumber-Body header to work
203+// around purego-cef's unexported PostData element wrapper.
194204 func (ci *contentInjector) injectMessageBridgeShim(wv *WebView) {
195- const script = `(function() {
196- window.dumber = window.dumber || {};
197- window.dumber.postMessage = function(msg) {
198- return fetch('/api/message', {
199- method: 'POST',
200- headers: {'Content-Type': 'application/json'},
201- body: JSON.stringify(msg)
202- }).then(function(r) { return r.json(); }).then(function(resp) {
203- if (resp && resp._callback && window[resp._callback]) {
204- window[resp._callback](resp.data);
205- }
206- return resp;
207- });
208- };
209-})();`
210-
211- wv.RunJavaScript(context.Background(), script)
205+ wv.RunJavaScript(context.Background(), MessageBridgeJS)
212206 }
213207
214208 // escapeForJSString escapes a string for use inside a JS single-quoted string literal.
@@ -217,5 +211,7 @@ func escapeForJSString(s string) string {
217211 s = strings.ReplaceAll(s, "'", "\\'")
218212 s = strings.ReplaceAll(s, "\n", "\\n")
219213 s = strings.ReplaceAll(s, "\r", "\\r")
214+ s = strings.ReplaceAll(s, "\u2028", "\\u2028")
215+ s = strings.ReplaceAll(s, "\u2029", "\\u2029")
220216 return s
221217 }
@@ -15,7 +15,7 @@ var _ port.ContentInjector = (*contentInjector)(nil)
15 15
16 // internalSchemePrefix is used to detect internal pages that receive dark mode16 // internalSchemePrefix is used to detect internal pages that receive dark mode
17 // and message bridge scripts. Must match the engine's InternalSchemePath prefix.17 // and message bridge scripts. Must match the engine's InternalSchemePath prefix.
18-const internalSchemePrefix = "cef://dumber"18+const internalSchemePrefix = "dumb://"
19 19
20 // contentInjector implements port.ContentInjector for the CEF engine.20 // contentInjector implements port.ContentInjector for the CEF engine.
21 // It stores CSS strings and injects them into webviews via ExecuteJavaScript.21 // It stores CSS strings and injects them into webviews via ExecuteJavaScript.
@@ -29,6 +29,13 @@ type contentInjector struct {
29 colorResolver port.ColorSchemeResolver29 colorResolver port.ColorSchemeResolver
30 }30 }
31 31
32+// setColorResolver updates the color scheme resolver used for dark mode detection.
33+func (ci *contentInjector) setColorResolver(resolver port.ColorSchemeResolver) {
34+ ci.mu.Lock()
35+ defer ci.mu.Unlock()
36+ ci.colorResolver = resolver
37+}
38+
32 // newContentInjector creates a content injector wired to the given engine.39 // newContentInjector creates a content injector wired to the given engine.
33 func newContentInjector(engine *Engine, resolver port.ColorSchemeResolver) *contentInjector {40 func newContentInjector(engine *Engine, resolver port.ColorSchemeResolver) *contentInjector {
34 return &contentInjector{41 return &contentInjector{
@@ -125,12 +132,13 @@ func (ci *contentInjector) onLoadEnd(wv *WebView) {
125 132
126 // injectCSS injects a CSS string as a <style> element via JavaScript.133 // injectCSS injects a CSS string as a <style> element via JavaScript.
127 func (ci *contentInjector) injectCSS(wv *WebView, id, css string) {134 func (ci *contentInjector) injectCSS(wv *WebView, id, css string) {
135+ escapedID := escapeForJSString(id)
128 escaped := escapeForJSString(css)136 escaped := escapeForJSString(css)
129 script := fmt.Sprintf(`(function(){137 script := fmt.Sprintf(`(function(){
130 var el = document.getElementById('%s');138 var el = document.getElementById('%s');
131 if (!el) { el = document.createElement('style'); el.id = '%s'; document.head.appendChild(el); }139 if (!el) { el = document.createElement('style'); el.id = '%s'; document.head.appendChild(el); }
132 el.textContent = '%s';140 el.textContent = '%s';
133-})();`, id, id, escaped)141+})();`, escapedID, escapedID, escaped)
134 142
135 wv.RunJavaScript(context.Background(), script)143 wv.RunJavaScript(context.Background(), script)
136 }144 }
@@ -191,24 +199,10 @@ func (ci *contentInjector) injectDarkModeScript(wv *WebView, prefersDark bool) {
191 199
192 // injectMessageBridgeShim injects the window.dumber.postMessage JS client shim200 // injectMessageBridgeShim injects the window.dumber.postMessage JS client shim
193 // so internal pages can communicate with Go handlers via fetch.201 // so internal pages can communicate with Go handlers via fetch.
202+// The message body is base64-encoded into the X-Dumber-Body header to work
203+// around purego-cef's unexported PostData element wrapper.
194 func (ci *contentInjector) injectMessageBridgeShim(wv *WebView) {204 func (ci *contentInjector) injectMessageBridgeShim(wv *WebView) {
195- const script = `(function() {205+ wv.RunJavaScript(context.Background(), MessageBridgeJS)
196- window.dumber = window.dumber || {};
197- window.dumber.postMessage = function(msg) {
198- return fetch('/api/message', {
199- method: 'POST',
200- headers: {'Content-Type': 'application/json'},
201- body: JSON.stringify(msg)
202- }).then(function(r) { return r.json(); }).then(function(resp) {
203- if (resp && resp._callback && window[resp._callback]) {
204- window[resp._callback](resp.data);
205- }
206- return resp;
207- });
208- };
209-})();`
210-
211- wv.RunJavaScript(context.Background(), script)
212 }206 }
213 207
214 // escapeForJSString escapes a string for use inside a JS single-quoted string literal.208 // escapeForJSString escapes a string for use inside a JS single-quoted string literal.
@@ -217,5 +211,7 @@ func escapeForJSString(s string) string {
217 s = strings.ReplaceAll(s, "'", "\\'")211 s = strings.ReplaceAll(s, "'", "\\'")
218 s = strings.ReplaceAll(s, "\n", "\\n")212 s = strings.ReplaceAll(s, "\n", "\\n")
219 s = strings.ReplaceAll(s, "\r", "\\r")213 s = strings.ReplaceAll(s, "\r", "\\r")
214+ s = strings.ReplaceAll(s, "\u2028", "\\u2028")
215+ s = strings.ReplaceAll(s, "\u2029", "\\u2029")
220 return s216 return s
221 }217 }
M internal/infrastructure/cef/engine.go +2 -6
@@ -97,9 +97,7 @@ func (e *Engine) FaviconDatabase() port.FaviconDatabase {
9797 // engine creation (e.g., from bootstrap wiring).
9898 func (e *Engine) SetColorResolver(resolver port.ColorSchemeResolver) {
9999 if e.contentInj != nil {
100- e.contentInj.mu.Lock()
101- e.contentInj.colorResolver = resolver
102- e.contentInj.mu.Unlock()
100+ e.contentInj.setColorResolver(resolver)
103101 }
104102 }
105103
@@ -138,15 +136,13 @@ func (e *Engine) RegisterHandlers(ctx context.Context, deps port.HandlerDependen
138136 return nil
139137 }
140138 log := logging.FromContext(ctx)
141- log.Debug().Msg("cef: registering message handlers (deps available, handler wiring pending)")
142-
143139 // TODO: Wire homepage, config, keybindings, and clipboard handlers
144140 // into e.messageRouter. Each handler implements MessageHandler and is
145141 // registered by message type (e.g. "get_recent_history", "get_favorites").
146142 // The CEF MessageRouter dispatches based on Message.Type from the JS bridge.
147143 _ = deps
148144
149- log.Info().Msg("cef: message handlers registered")
145+ log.Warn().Msg("cef: RegisterHandlers stub — handler wiring not yet implemented")
150146 return nil
151147 }
152148
@@ -97,9 +97,7 @@ func (e *Engine) FaviconDatabase() port.FaviconDatabase {
97 // engine creation (e.g., from bootstrap wiring).97 // engine creation (e.g., from bootstrap wiring).
98 func (e *Engine) SetColorResolver(resolver port.ColorSchemeResolver) {98 func (e *Engine) SetColorResolver(resolver port.ColorSchemeResolver) {
99 if e.contentInj != nil {99 if e.contentInj != nil {
100- e.contentInj.mu.Lock()100+ e.contentInj.setColorResolver(resolver)
101- e.contentInj.colorResolver = resolver
102- e.contentInj.mu.Unlock()
103 }101 }
104 }102 }
105 103
@@ -138,15 +136,13 @@ func (e *Engine) RegisterHandlers(ctx context.Context, deps port.HandlerDependen
138 return nil136 return nil
139 }137 }
140 log := logging.FromContext(ctx)138 log := logging.FromContext(ctx)
141- log.Debug().Msg("cef: registering message handlers (deps available, handler wiring pending)")
142-
143 // TODO: Wire homepage, config, keybindings, and clipboard handlers139 // TODO: Wire homepage, config, keybindings, and clipboard handlers
144 // into e.messageRouter. Each handler implements MessageHandler and is140 // into e.messageRouter. Each handler implements MessageHandler and is
145 // registered by message type (e.g. "get_recent_history", "get_favorites").141 // registered by message type (e.g. "get_recent_history", "get_favorites").
146 // The CEF MessageRouter dispatches based on Message.Type from the JS bridge.142 // The CEF MessageRouter dispatches based on Message.Type from the JS bridge.
147 _ = deps143 _ = deps
148 144
149- log.Info().Msg("cef: message handlers registered")145+ log.Warn().Msg("cef: RegisterHandlers stub — handler wiring not yet implemented")
150 return nil146 return nil
151 }147 }
152 148
M internal/infrastructure/cef/engine_init.go +6 -1
@@ -33,6 +33,7 @@ func NewEngine(ctx context.Context, cfg config.CEFEngineConfig) (*Engine, error)
3333 settings := prepareCEFSettings(cfg, logger)
3434
3535 // Inject --no-zygote temporarily for cef_initialize, then restore.
36+ // Safe: runs during single-threaded startup before concurrent goroutines.
3637 savedArgs := os.Args
3738 os.Args = appendIfMissing(os.Args, "--no-zygote")
3839
@@ -144,7 +145,11 @@ func wireEngine(
144145 schemeHandler.setAssets(assets.WebUIAssets)
145146
146147 result := purecef.RegisterSchemeHandlerFactory("dumb", "", purecef.NewSchemeHandlerFactory(schemeHandler))
147- logger.Info().Int32("result", result).Msg("cef: registered dumb:// scheme handler factory")
148+ if result != 1 {
149+ logger.Error().Int32("result", result).Msg("cef: failed to register dumb:// scheme handler factory")
150+ } else {
151+ logger.Info().Msg("cef: registered dumb:// scheme handler factory")
152+ }
148153
149154 eng.messageRouter = messageRouter
150155 eng.schemeHandler = schemeHandler
@@ -33,6 +33,7 @@ func NewEngine(ctx context.Context, cfg config.CEFEngineConfig) (*Engine, error)
33 settings := prepareCEFSettings(cfg, logger)33 settings := prepareCEFSettings(cfg, logger)
34 34
35 // Inject --no-zygote temporarily for cef_initialize, then restore.35 // Inject --no-zygote temporarily for cef_initialize, then restore.
36+ // Safe: runs during single-threaded startup before concurrent goroutines.
36 savedArgs := os.Args37 savedArgs := os.Args
37 os.Args = appendIfMissing(os.Args, "--no-zygote")38 os.Args = appendIfMissing(os.Args, "--no-zygote")
38 39
@@ -144,7 +145,11 @@ func wireEngine(
144 schemeHandler.setAssets(assets.WebUIAssets)145 schemeHandler.setAssets(assets.WebUIAssets)
145 146
146 result := purecef.RegisterSchemeHandlerFactory("dumb", "", purecef.NewSchemeHandlerFactory(schemeHandler))147 result := purecef.RegisterSchemeHandlerFactory("dumb", "", purecef.NewSchemeHandlerFactory(schemeHandler))
147- logger.Info().Int32("result", result).Msg("cef: registered dumb:// scheme handler factory")148+ if result != 1 {
149+ logger.Error().Int32("result", result).Msg("cef: failed to register dumb:// scheme handler factory")
150+ } else {
151+ logger.Info().Msg("cef: registered dumb:// scheme handler factory")
152+ }
148 153
149 eng.messageRouter = messageRouter154 eng.messageRouter = messageRouter
150 eng.schemeHandler = schemeHandler155 eng.schemeHandler = schemeHandler
M internal/infrastructure/cef/find_controller.go +6 -5
@@ -16,7 +16,6 @@ type cefFindController struct {
1616 mu sync.RWMutex
1717 host purecef.BrowserHost
1818 searchTxt string
19- lastID int32 // incremental identifier for the current search
2019 matchCase int32 // cached from most recent Search call
2120
2221 // counting tracks whether the current search is a CountMatches request
@@ -48,7 +47,6 @@ func (fc *cefFindController) setHost(host purecef.BrowserHost) {
4847 func (fc *cefFindController) Search(text string, opts port.FindOptions, _ uint) {
4948 fc.mu.Lock()
5049 fc.searchTxt = text
51- fc.lastID++
5250 fc.counting = false
5351 mc := matchCaseFromOpts(opts)
5452 fc.matchCase = mc
@@ -65,7 +63,6 @@ func (fc *cefFindController) Search(text string, opts port.FindOptions, _ uint)
6563 func (fc *cefFindController) CountMatches(text string, opts port.FindOptions, _ uint) {
6664 fc.mu.Lock()
6765 fc.searchTxt = text
68- fc.lastID++
6966 fc.counting = true
7067 mc := matchCaseFromOpts(opts)
7168 fc.matchCase = mc
@@ -206,14 +203,18 @@ func (fc *cefFindController) handleFindResult(_, count, _, finalUpdate int32) {
206203 }
207204 fc.mu.RUnlock()
208205
206+ var safeCount uint
207+ if count > 0 {
208+ safeCount = uint(count)
209+ }
209210 for _, cb := range foundCBs {
210- cb(uint(count))
211+ cb(safeCount)
211212 }
212213 for _, cb := range failedCBs {
213214 cb()
214215 }
215216 for _, cb := range countedCBs {
216- cb(uint(count))
217+ cb(safeCount)
217218 }
218219 }
219220
@@ -16,7 +16,6 @@ type cefFindController struct {
16 mu sync.RWMutex16 mu sync.RWMutex
17 host purecef.BrowserHost17 host purecef.BrowserHost
18 searchTxt string18 searchTxt string
19- lastID int32 // incremental identifier for the current search
20 matchCase int32 // cached from most recent Search call19 matchCase int32 // cached from most recent Search call
21 20
22 // counting tracks whether the current search is a CountMatches request21 // counting tracks whether the current search is a CountMatches request
@@ -48,7 +47,6 @@ func (fc *cefFindController) setHost(host purecef.BrowserHost) {
48 func (fc *cefFindController) Search(text string, opts port.FindOptions, _ uint) {47 func (fc *cefFindController) Search(text string, opts port.FindOptions, _ uint) {
49 fc.mu.Lock()48 fc.mu.Lock()
50 fc.searchTxt = text49 fc.searchTxt = text
51- fc.lastID++
52 fc.counting = false50 fc.counting = false
53 mc := matchCaseFromOpts(opts)51 mc := matchCaseFromOpts(opts)
54 fc.matchCase = mc52 fc.matchCase = mc
@@ -65,7 +63,6 @@ func (fc *cefFindController) Search(text string, opts port.FindOptions, _ uint)
65 func (fc *cefFindController) CountMatches(text string, opts port.FindOptions, _ uint) {63 func (fc *cefFindController) CountMatches(text string, opts port.FindOptions, _ uint) {
66 fc.mu.Lock()64 fc.mu.Lock()
67 fc.searchTxt = text65 fc.searchTxt = text
68- fc.lastID++
69 fc.counting = true66 fc.counting = true
70 mc := matchCaseFromOpts(opts)67 mc := matchCaseFromOpts(opts)
71 fc.matchCase = mc68 fc.matchCase = mc
@@ -206,14 +203,18 @@ func (fc *cefFindController) handleFindResult(_, count, _, finalUpdate int32) {
206 }203 }
207 fc.mu.RUnlock()204 fc.mu.RUnlock()
208 205
206+ var safeCount uint
207+ if count > 0 {
208+ safeCount = uint(count)
209+ }
209 for _, cb := range foundCBs {210 for _, cb := range foundCBs {
210- cb(uint(count))211+ cb(safeCount)
211 }212 }
212 for _, cb := range failedCBs {213 for _, cb := range failedCBs {
213 cb()214 cb()
214 }215 }
215 for _, cb := range countedCBs {216 for _, cb := range countedCBs {
216- cb(uint(count))217+ cb(safeCount)
217 }218 }
218 }219 }
219 220
M internal/infrastructure/cef/messaging.go +8 -3
@@ -64,7 +64,9 @@ func (r *MessageRouter) SetBaseContext(ctx context.Context) {
6464 if ctx == nil {
6565 ctx = context.Background()
6666 }
67+ r.mu.Lock()
6768 r.baseCtx = ctx
69+ r.mu.Unlock()
6870 }
6971
7072 // RegisterHandler registers a handler for a message type.
@@ -108,7 +110,10 @@ func (r *MessageRouter) RegisterHandlerWithCallbacks(msgType, callback, errorCal
108110 // HandleMessage decodes a raw JSON message body and routes it to the correct handler.
109111 // Returns the JSON-encoded response. Called by the scheme handler for /api/message POST.
110112 func (r *MessageRouter) HandleMessage(ctx context.Context, webviewID uint64, body []byte) ([]byte, error) {
111- log := logging.FromContext(r.baseCtx).With().Str("component", "message-router").Logger()
113+ r.mu.RLock()
114+ baseCtx := r.baseCtx
115+ r.mu.RUnlock()
116+ log := logging.FromContext(baseCtx).With().Str("component", "message-router").Logger()
112117
113118 var msg Message
114119 if err := json.Unmarshal(body, &msg); err != nil {
@@ -133,14 +138,14 @@ func (r *MessageRouter) HandleMessage(ctx context.Context, webviewID uint64, bod
133138 return json.Marshal(map[string]string{"error": "unknown message type: " + msg.Type})
134139 }
135140
136- log.Info().
141+ log.Debug().
137142 Str("type", msg.Type).
138143 Uint64("webview_id", msg.WebViewID).
139144 Int("payload_len", len(msg.Payload)).
140145 Msg("dispatching message to handler")
141146
142147 if ctx == nil {
143- ctx = r.baseCtx
148+ ctx = baseCtx
144149 }
145150 resp, err := entry.handler.Handle(ctx, msg.WebViewID, msg.Payload)
146151 if err != nil {
@@ -64,7 +64,9 @@ func (r *MessageRouter) SetBaseContext(ctx context.Context) {
64 if ctx == nil {64 if ctx == nil {
65 ctx = context.Background()65 ctx = context.Background()
66 }66 }
67+ r.mu.Lock()
67 r.baseCtx = ctx68 r.baseCtx = ctx
69+ r.mu.Unlock()
68 }70 }
69 71
70 // RegisterHandler registers a handler for a message type.72 // RegisterHandler registers a handler for a message type.
@@ -108,7 +110,10 @@ func (r *MessageRouter) RegisterHandlerWithCallbacks(msgType, callback, errorCal
108 // HandleMessage decodes a raw JSON message body and routes it to the correct handler.110 // HandleMessage decodes a raw JSON message body and routes it to the correct handler.
109 // Returns the JSON-encoded response. Called by the scheme handler for /api/message POST.111 // Returns the JSON-encoded response. Called by the scheme handler for /api/message POST.
110 func (r *MessageRouter) HandleMessage(ctx context.Context, webviewID uint64, body []byte) ([]byte, error) {112 func (r *MessageRouter) HandleMessage(ctx context.Context, webviewID uint64, body []byte) ([]byte, error) {
111- log := logging.FromContext(r.baseCtx).With().Str("component", "message-router").Logger()113+ r.mu.RLock()
114+ baseCtx := r.baseCtx
115+ r.mu.RUnlock()
116+ log := logging.FromContext(baseCtx).With().Str("component", "message-router").Logger()
112 117
113 var msg Message118 var msg Message
114 if err := json.Unmarshal(body, &msg); err != nil {119 if err := json.Unmarshal(body, &msg); err != nil {
@@ -133,14 +138,14 @@ func (r *MessageRouter) HandleMessage(ctx context.Context, webviewID uint64, bod
133 return json.Marshal(map[string]string{"error": "unknown message type: " + msg.Type})138 return json.Marshal(map[string]string{"error": "unknown message type: " + msg.Type})
134 }139 }
135 140
136- log.Info().141+ log.Debug().
137 Str("type", msg.Type).142 Str("type", msg.Type).
138 Uint64("webview_id", msg.WebViewID).143 Uint64("webview_id", msg.WebViewID).
139 Int("payload_len", len(msg.Payload)).144 Int("payload_len", len(msg.Payload)).
140 Msg("dispatching message to handler")145 Msg("dispatching message to handler")
141 146
142 if ctx == nil {147 if ctx == nil {
143- ctx = r.baseCtx148+ ctx = baseCtx
144 }149 }
145 resp, err := entry.handler.Handle(ctx, msg.WebViewID, msg.Payload)150 resp, err := entry.handler.Handle(ctx, msg.WebViewID, msg.Payload)
146 if err != nil {151 if err != nil {
M internal/infrastructure/cef/scheme_handler.go +9 -3
@@ -36,6 +36,7 @@ type dumbSchemeHandler struct {
3636 ctx context.Context
3737 messageRouter *MessageRouter
3838 assets embed.FS
39+ assetsSet bool
3940 assetDir string
4041 logger zerolog.Logger
4142 mu sync.RWMutex
@@ -57,6 +58,7 @@ func (h *dumbSchemeHandler) setAssets(assets embed.FS) {
5758 h.mu.Lock()
5859 defer h.mu.Unlock()
5960 h.assets = assets
61+ h.assetsSet = true
6062 h.logger.Debug().Msg("assets filesystem configured")
6163 }
6264
@@ -129,7 +131,7 @@ func (h *dumbSchemeHandler) handleConfigAPI(cfg *config.Config) purecef.Resource
129131 // handleAsset serves static files from the embedded filesystem.
130132 func (h *dumbSchemeHandler) handleAsset(u *url.URL) purecef.ResourceHandler {
131133 h.mu.RLock()
132- hasAssets := h.assets != (embed.FS{})
134+ hasAssets := h.assetsSet
133135 assetDir := h.assetDir
134136 h.mu.RUnlock()
135137
@@ -265,12 +267,16 @@ func (h *dumbSchemeHandler) newRawResourceHandler(status int, contentType string
265267 }
266268
267269 func (h *dumbSchemeHandler) newErrorResourceHandler(status int, msg string) purecef.ResourceHandler {
268- body := fmt.Sprintf(`<!DOCTYPE html><html><body><h1>%d</h1><p>%s</p></body></html>`, status, msg)
270+ escaped := strings.ReplaceAll(strings.ReplaceAll(msg, "&", "&amp;"), "<", "&lt;")
271+ body := fmt.Sprintf(`<!DOCTYPE html><html><body><h1>%d</h1><p>%s</p></body></html>`, status, escaped)
269272 return h.newRawResourceHandler(status, "text/html; charset=utf-8", []byte(body))
270273 }
271274
272275 func (h *dumbSchemeHandler) newJSONResourceHandler(status int, v any) purecef.ResourceHandler {
273- data, _ := json.Marshal(v)
276+ data, err := json.Marshal(v)
277+ if err != nil {
278+ return h.newErrorResourceHandler(http.StatusInternalServerError, "JSON encoding failed")
279+ }
274280 return h.newRawResourceHandler(status, "application/json", data)
275281 }
276282
@@ -36,6 +36,7 @@ type dumbSchemeHandler struct {
36 ctx context.Context36 ctx context.Context
37 messageRouter *MessageRouter37 messageRouter *MessageRouter
38 assets embed.FS38 assets embed.FS
39+ assetsSet bool
39 assetDir string40 assetDir string
40 logger zerolog.Logger41 logger zerolog.Logger
41 mu sync.RWMutex42 mu sync.RWMutex
@@ -57,6 +58,7 @@ func (h *dumbSchemeHandler) setAssets(assets embed.FS) {
57 h.mu.Lock()58 h.mu.Lock()
58 defer h.mu.Unlock()59 defer h.mu.Unlock()
59 h.assets = assets60 h.assets = assets
61+ h.assetsSet = true
60 h.logger.Debug().Msg("assets filesystem configured")62 h.logger.Debug().Msg("assets filesystem configured")
61 }63 }
62 64
@@ -129,7 +131,7 @@ func (h *dumbSchemeHandler) handleConfigAPI(cfg *config.Config) purecef.Resource
129 // handleAsset serves static files from the embedded filesystem.131 // handleAsset serves static files from the embedded filesystem.
130 func (h *dumbSchemeHandler) handleAsset(u *url.URL) purecef.ResourceHandler {132 func (h *dumbSchemeHandler) handleAsset(u *url.URL) purecef.ResourceHandler {
131 h.mu.RLock()133 h.mu.RLock()
132- hasAssets := h.assets != (embed.FS{})134+ hasAssets := h.assetsSet
133 assetDir := h.assetDir135 assetDir := h.assetDir
134 h.mu.RUnlock()136 h.mu.RUnlock()
135 137
@@ -265,12 +267,16 @@ func (h *dumbSchemeHandler) newRawResourceHandler(status int, contentType string
265 }267 }
266 268
267 func (h *dumbSchemeHandler) newErrorResourceHandler(status int, msg string) purecef.ResourceHandler {269 func (h *dumbSchemeHandler) newErrorResourceHandler(status int, msg string) purecef.ResourceHandler {
268- body := fmt.Sprintf(`<!DOCTYPE html><html><body><h1>%d</h1><p>%s</p></body></html>`, status, msg)270+ escaped := strings.ReplaceAll(strings.ReplaceAll(msg, "&", "&amp;"), "<", "&lt;")
271+ body := fmt.Sprintf(`<!DOCTYPE html><html><body><h1>%d</h1><p>%s</p></body></html>`, status, escaped)
269 return h.newRawResourceHandler(status, "text/html; charset=utf-8", []byte(body))272 return h.newRawResourceHandler(status, "text/html; charset=utf-8", []byte(body))
270 }273 }
271 274
272 func (h *dumbSchemeHandler) newJSONResourceHandler(status int, v any) purecef.ResourceHandler {275 func (h *dumbSchemeHandler) newJSONResourceHandler(status int, v any) purecef.ResourceHandler {
273- data, _ := json.Marshal(v)276+ data, err := json.Marshal(v)
277+ if err != nil {
278+ return h.newErrorResourceHandler(http.StatusInternalServerError, "JSON encoding failed")
279+ }
274 return h.newRawResourceHandler(status, "application/json", data)280 return h.newRawResourceHandler(status, "application/json", data)
275 }281 }
276 282

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