Repository navigation
Expand file tree
/
Copy pathinput.go
More file actions
140 lines (131 loc) · 3.2 KB
/
Copy pathinput.go
File metadata and controls
140 lines (131 loc) · 3.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package main
import (
"encoding/json"
"net/url"
"strings"
)
// domainsFromInput gathers every domain/URL the buyer supplied across the incumbent's five
// aliases (urls, startUrls, domains, websites, targetUrls) plus the single-value "url" field,
// normalizes each into a fetchable URL + bare host, and dedupes while preserving order.
func domainsFromInput(in Input) (entries []normalizedEntry, rejected []string) {
seen := make(map[string]bool)
add := func(raw string) {
raw = strings.TrimSpace(raw)
if raw == "" {
return
}
norm, host, err := normalizeURL(raw)
if err != nil {
rejected = append(rejected, raw)
return
}
key := strings.ToLower(host)
if seen[key] {
return
}
seen[key] = true
entries = append(entries, normalizedEntry{url: norm, host: host, original: raw})
}
for _, raw := range coerceToList(in.Urls) {
add(raw)
}
for _, raw := range coerceToList(in.StartUrls) {
add(raw)
}
for _, raw := range coerceToList(in.Domains) {
add(raw)
}
for _, raw := range coerceToList(in.Websites) {
add(raw)
}
for _, raw := range coerceToList(in.TargetUrls) {
add(raw)
}
for _, raw := range coerceToList(in.URL) {
add(raw)
}
return entries, rejected
}
type normalizedEntry struct {
url string
host string
original string
}
// coerceToList mirrors the incumbent's documented input flexibility: a JSON array of strings,
// a JSON array of {url: "..."} objects, a single string, or a comma/newline-separated string
// are all accepted for every URL-bearing field.
func coerceToList(v interface{}) []string {
switch val := v.(type) {
case nil:
return nil
case string:
return splitStringList(val)
case []interface{}:
var out []string
for _, item := range val {
switch it := item.(type) {
case string:
out = append(out, it)
case map[string]interface{}:
if u, ok := it["url"].(string); ok {
out = append(out, u)
}
}
}
return out
case []string:
return val
default:
// Might be a raw json.RawMessage-shaped value coming through re-marshal; try once more.
b, err := json.Marshal(val)
if err != nil {
return nil
}
var arr []interface{}
if err := json.Unmarshal(b, &arr); err == nil {
return coerceToList(arr)
}
var s string
if err := json.Unmarshal(b, &s); err == nil {
return splitStringList(s)
}
return nil
}
}
func splitStringList(s string) []string {
s = strings.TrimSpace(s)
if s == "" {
return nil
}
fields := strings.FieldsFunc(s, func(r rune) bool {
return r == ',' || r == '\n' || r == '\r'
})
var out []string
for _, f := range fields {
f = strings.TrimSpace(f)
if f != "" {
out = append(out, f)
}
}
return out
}
// normalizeURL turns a bare host or full URL into a fetchable absolute URL plus its hostname.
// A scheme is added (https) when missing, matching the incumbent's documented behavior.
func normalizeURL(raw string) (absoluteURL string, host string, err error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", "", errEmptyInput
}
if !strings.Contains(raw, "://") {
raw = "https://" + raw
}
u, parseErr := url.Parse(raw)
if parseErr != nil {
return "", "", parseErr
}
h := u.Hostname()
if h == "" {
return "", "", errNoHost
}
return u.String(), h, nil
}