1package content
2
3import (
4 "context"
5 "time"
6
7 "github.com/bnema/dumber/internal/application/dto"
8 "github.com/bnema/dumber/internal/application/port"
9 "github.com/bnema/dumber/internal/domain/entity"
10 "github.com/bnema/dumber/internal/logging"
11)
12
13func (pm *popupManager) awaitPopupFeatures(
14 ctx context.Context,
15 hooks popupCoordinatorHooks,
16 parentPaneID entity.PaneID,
17 parentWebViewID port.WebViewID,
18 parentWebView port.WebView,
19 parentURIAtOpen string,
20 req port.PopupRequest,
21 decision dto.HostDecision,
22) port.WebView {
23 log := logging.FromContext(ctx)
24 normalized := buildPopupBrowsingContextRequest(req)
25 normalized.SourceHost = pm.resolveSourceHost(parentPaneID)
26 popupWV, err := pm.createPopupWebView(ctx, parentWebViewID, req.TargetURI, false)
27 if err != nil {
28 logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostFailed, err)
29 return nil
30 }
31 resolver, resolverOK := popupWV.(port.PopupFeatureResolver)
32 lifecycle, lifecycleOK := popupWV.(port.PopupLifecycleCapable)
33 if !resolverOK || !lifecycleOK || pm.onStagePopup == nil {
34 logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
35 popupWV.Destroy()
36 return nil
37 }
38 staging, err := pm.onStagePopup(ctx, StagePopupInput{PopupWebView: popupWV})
39 if err != nil || staging == nil {
40 logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostFailed, err)
41 popupWV.Destroy()
42 return nil
43 }
44 pm.setBrowsingContextDecision(popupWV, decision)
45 paneID, _ := pm.createPopupPane(popupWV.ID(), parentPaneID, req.TargetURI)
46 ownerWindowID := ""
47 if pm.windowIDForPane != nil {
48 ownerWindowID, _ = pm.windowIDForPane(parentPaneID)
49 }
50 pending := &deferredPendingPopup{
51 PendingPopup: &PendingPopup{
52 PaneID: paneID, WebView: popupWV, ParentPaneID: parentPaneID,
53 ParentWebViewID: parentWebViewID, TargetURI: req.TargetURI, FrameName: req.FrameName,
54 IsUserGesture: req.IsUserGesture, PopupType: DetectPopupType(req.FrameName), CreatedAt: time.Now(),
55 },
56 Request: req, Decision: decision, StagingHost: staging, OwnerWindowID: ownerWindowID,
57 ParentWebView: parentWebView,
58 }
59 pm.storeDeferredPopup(popupWV.ID(), pending)
60 callbackCtx := logging.WithContext(context.Background(), *log)
61 lifecycle.SetOnReadyToShow(func() {
62 pm.resolvePendingPopupFeatures(callbackCtx, hooks, popupWV.ID(), parentURIAtOpen, resolver)
63 })
64 lifecycle.SetOnClose(func() {
65 pm.failPendingPopup(callbackCtx, popupWV.ID(), dto.BrowsingContextFailureHostFailed, nil)
66 })
67 return popupWV
68}
69
70func (pm *popupManager) cleanupDeferredPending(pending *deferredPendingPopup) {
71 if pending == nil {
72 return
73 }
74 pending.cleanupOnce.Do(func() {
75 if lifecycle, ok := pending.WebView.(port.PopupLifecycleCapable); ok {
76 lifecycle.SetOnReadyToShow(nil)
77 lifecycle.SetOnClose(nil)
78 }
79 if pending.StagingHost != nil {
80 pending.StagingHost.Destroy()
81 }
82 if pending.WebView != nil {
83 pending.WebView.Destroy()
84 }
85 })
86}
87
88func (pm *popupManager) cancelDeferredPopupsMatching(match func(*deferredPendingPopup) bool) {
89 if pm == nil || match == nil {
90 return
91 }
92 pm.mu.Lock()
93 canceled := make([]*deferredPendingPopup, 0)
94 for popupID, pending := range pm.deferredPopups {
95 if pending == nil || !match(pending) {
96 continue
97 }
98 delete(pm.deferredPopups, popupID)
99 delete(pm.pendingPopups, popupID)
100 canceled = append(canceled, pending)
101 }
102 pm.mu.Unlock()
103 for _, pending := range canceled {
104 pm.cleanupDeferredPending(pending)
105 }
106}
107
108func (pm *popupManager) cancelDeferredPopupsForParent(parentPaneID entity.PaneID) {
109 pm.cancelDeferredPopupsMatching(func(pending *deferredPendingPopup) bool {
110 return pending.ParentPaneID == parentPaneID
111 })
112}
113
114func (pm *popupManager) cancelDeferredPopupsForWindow(windowID string) {
115 if pm == nil || windowID == "" {
116 return
117 }
118 pm.cancelDeferredPopupsMatching(func(pending *deferredPendingPopup) bool {
119 return pending.OwnerWindowID == windowID
120 })
121}
122
123func (pm *popupManager) cancelAllDeferredPopups() {
124 pm.cancelDeferredPopupsMatching(func(*deferredPendingPopup) bool { return true })
125}
126
127func (pm *popupManager) failPendingPopup(
128 ctx context.Context,
129 popupID port.WebViewID,
130 code dto.BrowsingContextFailureCode,
131 err error,
132) {
133 pending, ok := pm.takeDeferredPopup(popupID)
134 if !ok || pending == nil {
135 return
136 }
137 req := buildPopupBrowsingContextRequest(pending.Request)
138 req.SourceHost = pm.resolveSourceHost(pending.ParentPaneID)
139 logBrowsingContextFailure(*logging.FromContext(ctx), req, pending.Decision, code, err)
140 pm.cleanupDeferredPending(pending)
141}
142
143func (pm *popupManager) resolvePendingPopupFeatures(
144 ctx context.Context,
145 hooks popupCoordinatorHooks,
146 popupID port.WebViewID,
147 parentURIAtOpen string,
148 resolver port.PopupFeatureResolver,
149) {
150 pending, ok := pm.takeDeferredPopup(popupID)
151 if !ok || pending == nil {
152 return
153 }
154 resolved := resolver.ResolvePopupFeatures()
155 if resolved.State == dto.PopupFeaturesUnknown {
156 req := buildPopupBrowsingContextRequest(pending.Request)
157 req.SourceHost = pm.resolveSourceHost(pending.ParentPaneID)
158 logBrowsingContextFailure(*logging.FromContext(ctx), req, pending.Decision, dto.BrowsingContextFailureFeatureResolution, nil)
159 pm.cleanupDeferredPending(pending)
160 return
161 }
162 if err := pending.StagingHost.Detach(); err != nil {
163 req := buildPopupBrowsingContextRequest(pending.Request)
164 req.SourceHost = pm.resolveSourceHost(pending.ParentPaneID)
165 logBrowsingContextFailure(*logging.FromContext(ctx), req, pending.Decision, dto.BrowsingContextFailureStagingDetach, err)
166 pm.cleanupDeferredPending(pending)
167 return
168 }
169
170 request := pending.Request
171 request.PopupFeatures = resolved
172 normalized := buildPopupBrowsingContextRequest(request)
173 normalized.SourceHost = pm.resolveSourceHost(pending.ParentPaneID)
174 namedContextExists := false
175 if !request.NoJavaScriptAccess {
176 _, namedContextExists = pm.lookupReusableNamedPopup(pending.ParentPaneID, request.FrameName, hooks)
177 }
178 decision := pm.policy.Decide(normalized, namedContextExists)
179 logBrowsingContextDecision(*logging.FromContext(ctx), normalized, decision)
180 pm.setBrowsingContextDecision(pending.WebView, decision)
181
182 transferred := false
183 switch decision.Kind {
184 case dto.HostDecisionNavigateSource:
185 if pending.ParentWebView == nil {
186 logBrowsingContextFailure(*logging.FromContext(ctx), normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
187 break
188 }
189 if err := pending.ParentWebView.LoadURI(ctx, request.TargetURI); err != nil {
190 logBrowsingContextFailure(*logging.FromContext(ctx), normalized, decision, dto.BrowsingContextFailureHostFailed, err)
191 }
192 case dto.HostDecisionCreateBrowserWindow:
193 transferred = pm.openExistingPopupInBrowserWindow(
194 ctx, hooks, pending.ParentPaneID, pending.ParentWebViewID,
195 pending.WebView, request, decision, true,
196 )
197 case dto.HostDecisionCreateNativePopup:
198 transferred = pm.openExistingPopupInNativePopup(
199 ctx, hooks, pending.ParentPaneID, pending.ParentWebViewID,
200 parentURIAtOpen, pending.WebView, request, decision,
201 )
202 case dto.HostDecisionReuseNamedPane:
203 // Reuse transfers no ownership: the existing context is navigated while
204 // the newly staged WebView remains ours and must be discarded below.
205 if _, ok := pm.reuseNamedPopup(ctx, hooks, pending.ParentPaneID, request.FrameName, request.TargetURI); !ok {
206 logBrowsingContextFailure(*logging.FromContext(ctx), normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
207 }
208 default:
209 logBrowsingContextFailure(*logging.FromContext(ctx), normalized, decision, dto.BrowsingContextFailureFeatureResolution, nil)
210 }
211 if !transferred {
212 pm.cleanupDeferredPending(pending)
213 }
214}