1package cef
2
3import (
4 "net/url"
5 "strings"
6
7 "golang.org/x/net/html"
8)
9
10// DiscoverFaviconCandidates extracts ordered favicon URL candidates from page
11// metadata and appends the origin /favicon.ico fallback. Only http(s) page and
12// icon URLs are returned; duplicates are removed while preserving order.
13func DiscoverFaviconCandidates(pageURL, htmlSource string) []string {
14 candidates := make([]string, 0, 4)
15 for _, href := range parseFaviconHrefs(htmlSource) {
16 if resolved, ok := resolveFaviconURL(pageURL, href); ok {
17 candidates = append(candidates, resolved)
18 }
19 }
20 if fallback, ok := originFaviconURL(pageURL); ok {
21 candidates = append(candidates, fallback)
22 }
23 if len(candidates) == 0 {
24 return nil
25 }
26 return dedupeStrings(candidates)
27}
28
29func resolveFaviconCandidates(pageURL string, rawIconURLs []string) []string {
30 candidates := make([]string, 0, len(rawIconURLs))
31 for _, raw := range rawIconURLs {
32 if resolved, ok := resolveFaviconURL(pageURL, raw); ok {
33 candidates = append(candidates, resolved)
34 }
35 }
36 return dedupeStrings(candidates)
37}
38
39func fallbackFaviconCandidates(pageURL string) []string {
40 fallback, ok := originFaviconURL(pageURL)
41 if !ok {
42 return nil
43 }
44 return []string{fallback}
45}
46
47func parseFaviconHrefs(htmlSource string) []string {
48 z := html.NewTokenizer(strings.NewReader(htmlSource))
49 var hrefs []string
50 for {
51 switch z.Next() {
52 case html.ErrorToken:
53 return hrefs
54 case html.StartTagToken, html.SelfClosingTagToken:
55 tn, hasAttr := z.TagName()
56 if !hasAttr || !strings.EqualFold(string(tn), "link") {
57 continue
58 }
59 var rel, href string
60 for {
61 key, val, more := z.TagAttr()
62 switch strings.ToLower(string(key)) {
63 case "rel":
64 rel = string(val)
65 case "href":
66 href = string(val)
67 }
68 if !more {
69 break
70 }
71 }
72 if href != "" && faviconRel(rel) {
73 hrefs = append(hrefs, href)
74 }
75 }
76 }
77}
78
79func faviconRel(rel string) bool {
80 fields := strings.FieldsSeq(strings.ToLower(rel))
81 for field := range fields {
82 if field == "icon" || field == "apple-touch-icon" || field == "apple-touch-icon-precomposed" {
83 return true
84 }
85 }
86 return false
87}
88
89func resolveFaviconURL(pageURL, rawIconURL string) (string, bool) {
90 trimmed := strings.TrimSpace(rawIconURL)
91 if trimmed == "" {
92 return "", false
93 }
94 icon, err := url.Parse(trimmed)
95 if err != nil {
96 return "", false
97 }
98 if icon.IsAbs() {
99 if !isHTTPScheme(icon.Scheme) || icon.Host == "" {
100 return "", false
101 }
102 return icon.String(), true
103 }
104 base, err := url.Parse(pageURL)
105 if err != nil || !isHTTPScheme(base.Scheme) || base.Host == "" {
106 return "", false
107 }
108 resolved := base.ResolveReference(icon)
109 if !isHTTPScheme(resolved.Scheme) || resolved.Host == "" {
110 return "", false
111 }
112 return resolved.String(), true
113}
114
115func originFaviconURL(pageURL string) (string, bool) {
116 u, err := url.Parse(pageURL)
117 if err != nil || !isHTTPScheme(u.Scheme) || u.Host == "" {
118 return "", false
119 }
120 u.Path = "/favicon.ico"
121 u.RawPath = ""
122 u.RawQuery = ""
123 u.Fragment = ""
124 return u.String(), true
125}
126
127func isHTTPScheme(scheme string) bool {
128 return scheme == "http" || scheme == "https"
129}
130
131func dedupeStrings(values []string) []string {
132 seen := make(map[string]struct{}, len(values))
133 out := make([]string, 0, len(values))
134 for _, value := range values {
135 if value == "" {
136 continue
137 }
138 if _, ok := seen[value]; ok {
139 continue
140 }
141 seen[value] = struct{}{}
142 out = append(out, value)
143 }
144 return out
145}