1package cef
2
3import (
4 "net/url"
5 "path"
6 "strings"
7)
8
9const (
10 conceptualInternalSchemePrefix = "dumb://"
11 actualInternalScheme = "https"
12 actualInternalHost = "dumber.invalid"
13 actualInternalOrigin = actualInternalScheme + "://" + actualInternalHost
14 internalAPIPathPrefix = "/api/"
15 internalAPIPathPrefixTrimmed = "api/"
16)
17
18func isConceptualInternalURL(raw string) bool {
19 return strings.HasPrefix(raw, conceptualInternalSchemePrefix) || strings.HasPrefix(raw, "dumb:")
20}
21
22func isActualInternalURL(raw string) bool {
23 parsed, err := url.Parse(raw)
24 if err != nil {
25 return false
26 }
27 return strings.EqualFold(parsed.Scheme, actualInternalScheme) && strings.EqualFold(parsed.Host, actualInternalHost)
28}
29
30func toActualInternalURL(raw string) string {
31 if raw == "" || isActualInternalURL(raw) {
32 return raw
33 }
34
35 parsed, err := url.Parse(raw)
36 if err != nil || !strings.EqualFold(parsed.Scheme, "dumb") {
37 return raw
38 }
39
40 page := parsed.Host
41 if page == "" {
42 page = parsed.Opaque
43 }
44 if page == "" || !isInternalPageHost(page) {
45 return raw
46 }
47
48 actualPath := "/" + page
49 trimmedPath := strings.Trim(parsed.Path, "/")
50 switch {
51 case trimmedPath == "":
52 // Keep page roots at /history instead of /history/ so relative assets
53 // resolve from the origin root.
54 case strings.HasPrefix(trimmedPath, internalAPIPathPrefixTrimmed):
55 actualPath = "/" + trimmedPath
56 case path.Ext(trimmedPath) != "":
57 actualPath = "/" + trimmedPath
58 default:
59 actualPath = "/" + page + "/" + trimmedPath
60 }
61
62 actual := url.URL{
63 Scheme: actualInternalScheme,
64 Host: actualInternalHost,
65 Path: actualPath,
66 RawQuery: parsed.RawQuery,
67 Fragment: parsed.Fragment,
68 }
69 return actual.String()
70}
71
72func resolveAPIPath(u *url.URL) (string, bool) {
73 if u == nil {
74 return "", false
75 }
76
77 if strings.EqualFold(u.Scheme, actualInternalScheme) && strings.EqualFold(u.Host, actualInternalHost) {
78 if strings.HasPrefix(u.Path, internalAPIPathPrefix) {
79 return u.Path, true
80 }
81 return "", false
82 }
83
84 if !strings.EqualFold(u.Scheme, "dumb") {
85 return "", false
86 }
87
88 if strings.EqualFold(u.Host, "api") {
89 if u.Path == "" {
90 return "/api", true
91 }
92 return internalAPIPathPrefix[:len(internalAPIPathPrefix)-1] + u.Path, true
93 }
94
95 if strings.HasPrefix(u.Path, internalAPIPathPrefix) {
96 return u.Path, true
97 }
98
99 return "", false
100}
101
102func toConceptualInternalURL(raw string) string {
103 if raw == "" || !isActualInternalURL(raw) {
104 return raw
105 }
106
107 parsed, err := url.Parse(raw)
108 if err != nil {
109 return raw
110 }
111
112 trimmedPath := strings.Trim(parsed.Path, "/")
113 if trimmedPath == "" {
114 return raw
115 }
116
117 parts := strings.SplitN(trimmedPath, "/", 2)
118 page := parts[0]
119 if !isInternalPageHost(page) {
120 return raw
121 }
122
123 conceptual := url.URL{
124 Scheme: "dumb",
125 Host: page,
126 RawQuery: parsed.RawQuery,
127 Fragment: parsed.Fragment,
128 }
129 if len(parts) == 2 && parts[1] != "" {
130 conceptual.Path = "/" + parts[1]
131 }
132 return conceptual.String()
133}
134
135func isInternalPageHost(host string) bool {
136 switch host {
137 case historyPath, favoritesPath, configPath, errorPath:
138 return true
139 default:
140 return false
141 }
142}