1package content
2
3import (
4 "context"
5 "errors"
6 "sync"
7
8 "github.com/bnema/dumber/internal/application/dto"
9 "github.com/bnema/dumber/internal/application/port"
10 "github.com/bnema/dumber/internal/domain/entity"
11 "github.com/bnema/dumber/internal/logging"
12)
13
14// openBrowserWindow transfers a related WebView to a complete browser window.
15// The popup manager retains ownership until the host callback succeeds.
16func (pm *popupManager) openBrowserWindow(
17 ctx context.Context,
18 hooks popupCoordinatorHooks,
19 parentPaneID entity.PaneID,
20 parentWebViewID port.WebViewID,
21 req port.PopupRequest,
22 decision dto.HostDecision,
23 ready bool,
24) port.WebView {
25 log := logging.FromContext(ctx)
26 normalized := buildPopupBrowsingContextRequest(req)
27 normalized.SourceHost = pm.resolveSourceHost(parentPaneID)
28 if pm.onOpenBrowserWindow == nil {
29 logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
30 return nil
31 }
32 popupWV, err := pm.createPopupWebView(ctx, parentWebViewID, req.TargetURI, true)
33 if err != nil {
34 logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostFailed, err)
35 return nil
36 }
37 pm.setBrowsingContextDecision(popupWV, decision)
38 if !pm.adoptPopupInBrowserWindow(
39 ctx, hooks, parentPaneID, parentWebViewID, popupWV, req, decision, ready,
40 dto.BrowsingContextFailureHostUnavailable, dto.BrowsingContextFailureHostFailed,
41 "browser window host returned an empty window ID",
42 ) {
43 popupWV.Destroy()
44 return nil
45 }
46 if lifecycle, ok := popupWV.(port.PopupLifecycleCapable); ok {
47 lifecycle.PrimePopupNavigation(req.TargetURI)
48 }
49 return popupWV
50}
51
52// openExistingPopupInBrowserWindow attempts to transfer an already-created
53// WebView. On failure ownership remains with the caller.
54func (pm *popupManager) openExistingPopupInBrowserWindow(
55 ctx context.Context,
56 hooks popupCoordinatorHooks,
57 parentPaneID entity.PaneID,
58 parentWebViewID port.WebViewID,
59 popupWV port.WebView,
60 req port.PopupRequest,
61 decision dto.HostDecision,
62 ready bool,
63) bool {
64 return pm.adoptPopupInBrowserWindow(
65 ctx, hooks, parentPaneID, parentWebViewID, popupWV, req, decision, ready,
66 dto.BrowsingContextFailureFallback, dto.BrowsingContextFailureFallback,
67 "browser window fallback returned an empty window ID",
68 )
69}
70
71func (pm *popupManager) adoptPopupInBrowserWindow(
72 ctx context.Context,
73 hooks popupCoordinatorHooks,
74 parentPaneID entity.PaneID,
75 parentWebViewID port.WebViewID,
76 popupWV port.WebView,
77 req port.PopupRequest,
78 decision dto.HostDecision,
79 ready bool,
80 unavailableCode dto.BrowsingContextFailureCode,
81 failureCode dto.BrowsingContextFailureCode,
82 emptyWindowError string,
83) bool {
84 log := logging.FromContext(ctx)
85 normalized := buildPopupBrowsingContextRequest(req)
86 normalized.SourceHost = pm.resolveSourceHost(parentPaneID)
87 if popupWV == nil || pm.onOpenBrowserWindow == nil {
88 logBrowsingContextFailure(*log, normalized, decision, unavailableCode, nil)
89 return false
90 }
91
92 paneID, popupPane := pm.createPopupPane(popupWV.ID(), parentPaneID, req.TargetURI)
93 if hooks.setupWebViewCallbacks != nil {
94 hooks.setupWebViewCallbacks(ctx, paneID, popupWV)
95 }
96 result, err := pm.onOpenBrowserWindow(ctx, BrowserWindowInput{
97 ParentPaneID: parentPaneID, ParentWebViewID: parentWebViewID, PopupPane: popupPane,
98 PopupWebView: popupWV, TargetURI: req.TargetURI, Request: req, Ready: ready,
99 })
100 if err != nil || result.WindowID == "" {
101 if err == nil {
102 err = errors.New(emptyWindowError)
103 }
104 logBrowsingContextFailure(*log, normalized, decision, failureCode, err)
105 return false
106 }
107 if !req.NoJavaScriptAccess {
108 pm.storeReusableNamedPopupWithHost(parentPaneID, req.FrameName, paneID, popupWV, result.WindowID)
109 }
110 return true
111}
112
113func (pm *popupManager) navigatePopupSource(
114 ctx context.Context,
115 parentID port.WebViewID,
116 parentWV port.WebView,
117 req port.PopupRequest,
118 request dto.NewBrowsingContextRequest,
119 decision dto.HostDecision,
120) {
121 log := logging.FromContext(ctx)
122 popupWV, err := pm.createPopupWebView(ctx, parentID, req.TargetURI, false)
123 if err != nil {
124 logBrowsingContextFailure(*log, request, decision, dto.BrowsingContextFailureHostFailed, err)
125 return
126 }
127 pm.setBrowsingContextDecision(popupWV, decision)
128 popupWV.Destroy()
129 if err := parentWV.LoadURI(ctx, req.TargetURI); err != nil {
130 logBrowsingContextFailure(*log, request, decision, dto.BrowsingContextFailureHostFailed, err)
131 }
132}
133
134func (*popupManager) navigateSource(ctx context.Context, parentWV port.WebView, uri string) bool {
135 if err := parentWV.LoadURI(ctx, uri); err != nil {
136 logging.FromContext(ctx).Error().Err(err).
137 Str("uri", logging.TruncateURL(uri, logURLMaxLen)).
138 Msg("failed to load URI in source floating pane")
139 return false
140 }
141 return true
142}
143
144func popupSupportsOpenerBridge(wv port.WebView) bool {
145 _, ok := wv.(port.PopupOpenerCapable)
146 return ok
147}
148
149func (pm *popupManager) openNativePopup(
150 ctx context.Context,
151 hooks popupCoordinatorHooks,
152 parentPaneID entity.PaneID,
153 parentID port.WebViewID,
154 parentURIAtOpen string,
155 req port.PopupRequest,
156 decision dto.HostDecision,
157) port.WebView {
158 log := logging.FromContext(ctx)
159 normalized := buildPopupBrowsingContextRequest(req)
160 normalized.SourceHost = pm.resolveSourceHost(parentPaneID)
161 if pm.onOpenNativePopup == nil {
162 logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
163 return nil
164 }
165 popupWV, err := pm.createPopupWebView(ctx, parentID, req.TargetURI, false)
166 if err != nil {
167 logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostFailed, err)
168 return nil
169 }
170 pm.setBrowsingContextDecision(popupWV, decision)
171 if !pm.adoptPopupInNativePopup(
172 ctx, hooks, parentPaneID, parentID, parentURIAtOpen, popupWV, req, decision,
173 dto.BrowsingContextFailureNativeArm,
174 ) {
175 popupWV.Destroy()
176 return nil
177 }
178 return popupWV
179}
180
181// openExistingPopupInNativePopup attempts to transfer an already-created
182// WebView. On failure ownership remains with the caller.
183func (pm *popupManager) openExistingPopupInNativePopup(
184 ctx context.Context,
185 hooks popupCoordinatorHooks,
186 parentPaneID entity.PaneID,
187 parentWebViewID port.WebViewID,
188 parentURIAtOpen string,
189 popupWV port.WebView,
190 req port.PopupRequest,
191 decision dto.HostDecision,
192) bool {
193 return pm.adoptPopupInNativePopup(
194 ctx, hooks, parentPaneID, parentWebViewID, parentURIAtOpen, popupWV, req, decision,
195 dto.BrowsingContextFailureHostFailed,
196 )
197}
198
199func (pm *popupManager) adoptPopupInNativePopup(
200 ctx context.Context,
201 hooks popupCoordinatorHooks,
202 parentPaneID entity.PaneID,
203 parentWebViewID port.WebViewID,
204 parentURIAtOpen string,
205 popupWV port.WebView,
206 req port.PopupRequest,
207 decision dto.HostDecision,
208 failureCode dto.BrowsingContextFailureCode,
209) bool {
210 log := logging.FromContext(ctx)
211 normalized := buildPopupBrowsingContextRequest(req)
212 normalized.SourceHost = pm.resolveSourceHost(parentPaneID)
213 if popupWV == nil || pm.onOpenNativePopup == nil {
214 logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
215 return false
216 }
217 cfg := pm.currentPopupConfig()
218 allowFallback := !normalized.AuthIntent && !decision.RequiresNativeOpener &&
219 (req.NoJavaScriptAccess || popupSupportsOpenerBridge(popupWV))
220 var abortOnce sync.Once
221 abortResult := false
222 onAbort := func(abortCtx context.Context, abortedWV port.WebView) bool {
223 abortOnce.Do(func() {
224 if !allowFallback {
225 logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureNativeArm, nil)
226 return
227 }
228 abortResult = pm.openExistingPopupInBrowserWindow(
229 abortCtx, hooks, parentPaneID, parentWebViewID, abortedWV, req, decision, true,
230 )
231 })
232 return abortResult
233 }
234 if err := pm.onOpenNativePopup(ctx, NativePopupInput{
235 ParentPaneID: parentPaneID, ParentWebViewID: parentWebViewID, ParentURIAtOpen: parentURIAtOpen,
236 PopupWebView: popupWV, TargetURI: req.TargetURI, Request: req,
237 ObserveOAuthAutoClose: cfg != nil && cfg.OAuthAutoClose && IsOAuthURL(req.TargetURI),
238 AllowBrowserWindowFallback: allowFallback, OnNativeHostAbort: onAbort,
239 }); err != nil {
240 logBrowsingContextFailure(*log, normalized, decision, failureCode, err)
241 return false
242 }
243 if lifecycle, ok := popupWV.(port.PopupLifecycleCapable); ok {
244 lifecycle.PrimePopupNavigation(req.TargetURI)
245 }
246 return true
247}