1package usecase
2
3import (
4 "strings"
5
6 "github.com/bnema/dumber/internal/application/dto"
7)
8
9// BrowsingContextPolicy selects the product host for a normalized browsing-context request.
10type BrowsingContextPolicy struct{}
11
12// Decide returns the host decision without performing any I/O.
13func (BrowsingContextPolicy) Decide(req dto.NewBrowsingContextRequest, namedContextExists bool) dto.HostDecision {
14 decision := dto.HostDecision{
15 SourceHost: req.SourceHost,
16 RequestContextDisposition: req.RequestContextDisposition,
17 RequiresNativeOpener: req.RequiresNativeOpener,
18 }
19 if decision.RequestContextDisposition == "" {
20 decision.RequestContextDisposition = dto.RequestContextInheritParent
21 }
22
23 switch {
24 case strings.TrimSpace(req.TargetURI) == "":
25 return completeHostDecision(decision, dto.HostDecisionDeny, dto.HostDecisionReasonEmptyTarget, "empty target URI")
26 case req.AuthIntent:
27 return completeHostDecision(
28 decision,
29 dto.HostDecisionCreateNativePopup,
30 dto.HostDecisionReasonAuthNativePopup,
31 "authentication intent requires a related native popup",
32 )
33 case req.RequiresNativeOpener:
34 return completeHostDecision(
35 decision,
36 dto.HostDecisionCreateNativePopup,
37 dto.HostDecisionReasonNativeOpenerRequired,
38 "native opener relationship is required",
39 )
40 case isAmbiguousNativeBrowsingContext(req):
41 return completeHostDecision(
42 decision,
43 dto.HostDecisionCreateNativePopup,
44 dto.HostDecisionReasonAmbiguousOpenerNative,
45 "ambiguous opener-coupled request prefers a related native popup",
46 )
47 }
48
49 name := ReusableBrowsingContextName(req.TargetFrameName)
50 if namedContextExists && name != "" && !req.NoJavaScriptAccess {
51 decision.ReuseContextName = name
52 decision.BrowsingContextName = name
53 return completeHostDecision(
54 decision,
55 dto.HostDecisionReuseNamedPane,
56 dto.HostDecisionReasonNamedContextReuse,
57 "reuse live named browsing context",
58 )
59 }
60 decision.BrowsingContextName = name
61
62 if req.SourceHost == dto.SourceHostFloating {
63 return decideFloatingHost(decision, req)
64 }
65 return completeHostDecision(
66 decision,
67 dto.HostDecisionCreatePane,
68 dto.HostDecisionReasonWorkspacePane,
69 "workspace request uses pane hosting",
70 )
71}
72
73func decideFloatingHost(decision dto.HostDecision, req dto.NewBrowsingContextRequest) dto.HostDecision {
74 switch {
75 case req.PopupFeatures.State == dto.PopupFeaturesUnknown && req.TriggerKind != dto.TriggerLinkNewPage:
76 return completeHostDecision(
77 decision,
78 dto.HostDecisionAwaitPopupFeatures,
79 dto.HostDecisionReasonFloatingFeaturesPending,
80 "popup features unavailable until ready-to-show",
81 )
82 case popupFeaturesRequestHost(req.PopupFeatures):
83 return completeHostDecision(
84 decision,
85 dto.HostDecisionCreateNativePopup,
86 dto.HostDecisionReasonFloatingPopupFeatures,
87 "explicit popup features requested",
88 )
89 case ReusableBrowsingContextName(req.TargetFrameName) == "":
90 return completeHostDecision(
91 decision,
92 dto.HostDecisionNavigateSource,
93 dto.HostDecisionReasonFloatingFeaturelessBlank,
94 "featureless blank request navigates the source floating pane",
95 )
96 default:
97 return completeHostDecision(
98 decision,
99 dto.HostDecisionCreateBrowserWindow,
100 dto.HostDecisionReasonFloatingBrowserWindow,
101 "floating request opens a detached browser window",
102 )
103 }
104}
105
106func popupFeaturesRequestHost(features dto.PopupFeatures) bool {
107 return features.State == dto.PopupFeaturesSpecified &&
108 ((features.WidthSet && features.Width > 0) ||
109 (features.HeightSet && features.Height > 0) ||
110 (features.ToolbarVisibilitySet && !features.ToolbarVisible) ||
111 (features.LocationbarVisibilitySet && !features.LocationbarVisible) ||
112 (features.ResizableSet && !features.Resizable) ||
113 (features.IsPopupSet && features.IsPopup))
114}
115
116func completeHostDecision(
117 decision dto.HostDecision,
118 kind dto.HostDecisionKind,
119 code dto.HostDecisionReasonCode,
120 reason string,
121) dto.HostDecision {
122 decision.Kind = kind
123 decision.ReasonCode = code
124 decision.Reason = reason
125 return decision
126}
127
128func isAmbiguousNativeBrowsingContext(req dto.NewBrowsingContextRequest) bool {
129 return req.TriggerKind == dto.TriggerUnknown && !req.NoJavaScriptAccess
130}
131
132// ReusableBrowsingContextName returns a live-context key for named targets.
133func ReusableBrowsingContextName(frameName string) string {
134 name := strings.TrimSpace(frameName)
135 if name == "" || strings.EqualFold(name, "_blank") {
136 return ""
137 }
138 return name
139}