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, false,
174 ) {
175 popupWV.Destroy()
176 return nil
177 }
178 return popupWV
179}
180
181// openExistingPopupInNativePopup attempts to transfer an already-created
182// WebView whose ready-to-show event has already fired (deferred feature
183// resolution). On failure ownership remains with the caller.
184func (pm *popupManager) openExistingPopupInNativePopup(
185 ctx context.Context,
186 hooks popupCoordinatorHooks,
187 parentPaneID entity.PaneID,
188 parentWebViewID port.WebViewID,
189 parentURIAtOpen string,
190 popupWV port.WebView,
191 req port.PopupRequest,
192 decision dto.HostDecision,
193) bool {
194 return pm.adoptPopupInNativePopup(
195 ctx, hooks, parentPaneID, parentWebViewID, parentURIAtOpen, popupWV, req, decision,
196 dto.BrowsingContextFailureHostFailed, true,
197 )
198}
199
200func (pm *popupManager) adoptPopupInNativePopup(
201 ctx context.Context,
202 hooks popupCoordinatorHooks,
203 parentPaneID entity.PaneID,
204 parentWebViewID port.WebViewID,
205 parentURIAtOpen string,
206 popupWV port.WebView,
207 req port.PopupRequest,
208 decision dto.HostDecision,
209 failureCode dto.BrowsingContextFailureCode,
210 alreadyReadyToShow bool,
211) bool {
212 log := logging.FromContext(ctx)
213 normalized := buildPopupBrowsingContextRequest(req)
214 normalized.SourceHost = pm.resolveSourceHost(parentPaneID)
215 if popupWV == nil || pm.onOpenNativePopup == nil {
216 logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
217 return false
218 }
219 cfg := pm.currentPopupConfig()
220 allowFallback := !normalized.AuthIntent && !decision.RequiresNativeOpener &&
221 (req.NoJavaScriptAccess || popupSupportsOpenerBridge(popupWV))
222 var abortOnce sync.Once
223 abortResult := false
224 onAbort := func(abortCtx context.Context, abortedWV port.WebView) bool {
225 abortOnce.Do(func() {
226 if !allowFallback {
227 logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureNativeArm, nil)
228 return
229 }
230 abortResult = pm.openExistingPopupInBrowserWindow(
231 abortCtx, hooks, parentPaneID, parentWebViewID, abortedWV, req, decision, true,
232 )
233 })
234 return abortResult
235 }
236 if err := pm.onOpenNativePopup(ctx, NativePopupInput{
237 ParentPaneID: parentPaneID, ParentWebViewID: parentWebViewID, ParentURIAtOpen: parentURIAtOpen,
238 PopupWebView: popupWV, TargetURI: req.TargetURI, Request: req,
239 ObserveOAuthAutoClose: cfg != nil && cfg.OAuthAutoClose && IsOAuthURL(req.TargetURI),
240 AllowBrowserWindowFallback: allowFallback, OnNativeHostAbort: onAbort,
241 AlreadyReadyToShow: alreadyReadyToShow,
242 }); err != nil {
243 logBrowsingContextFailure(*log, normalized, decision, failureCode, err)
244 return false
245 }
246 if lifecycle, ok := popupWV.(port.PopupLifecycleCapable); ok {
247 lifecycle.PrimePopupNavigation(req.TargetURI)
248 }
249 return true
250}