diff --git a/object/user_avatar.go b/object/user_avatar.go index 6d5ccb0c..be5474fc 100644 --- a/object/user_avatar.go +++ b/object/user_avatar.go @@ -54,19 +54,24 @@ func downloadImage(client *http.Client, url string) (*bytes.Buffer, string, erro // Get the content type and determine the file extension contentType := resp.Header.Get("Content-Type") fileExtension := "" - switch contentType { - case "image/jpeg": - fileExtension = ".jpg" - case "image/png": - fileExtension = ".png" - case "image/gif": - fileExtension = ".gif" - case "image/vnd.microsoft.icon": - fileExtension = ".ico" - case "image/x-icon": - fileExtension = ".ico" - default: - return nil, "", fmt.Errorf("unsupported content type: %s", contentType) + + if strings.Contains(contentType, "text/html") { + fileExtension = ".html" + } else { + switch contentType { + case "image/jpeg": + fileExtension = ".jpg" + case "image/png": + fileExtension = ".png" + case "image/gif": + fileExtension = ".gif" + case "image/vnd.microsoft.icon": + fileExtension = ".ico" + case "image/x-icon": + fileExtension = ".ico" + default: + return nil, "", fmt.Errorf("unsupported content type: %s", contentType) + } } // Save the image to a bytes.Buffer diff --git a/object/user_avatar_favicon.go b/object/user_avatar_favicon.go index b76e0207..51002e85 100644 --- a/object/user_avatar_favicon.go +++ b/object/user_avatar_favicon.go @@ -18,9 +18,173 @@ import ( "bytes" "fmt" "net/http" + "net/url" "strings" + + "golang.org/x/net/html" ) +type Link struct { + Rel string + Sizes string + Href string +} + +func GetFaviconUrl(htmlStr string) (string, error) { + doc, err := html.Parse(strings.NewReader(htmlStr)) + if err != nil { + return "", err + } + + var links []Link + findLinks(doc, &links) + + if len(links) == 0 { + return "", fmt.Errorf("no Favicon links found") + } + + chosenLink := chooseFaviconLink(links) + if chosenLink == nil { + return "", fmt.Errorf("unable to determine favicon URL") + } + + return chosenLink.Href, nil +} + +func findLinks(n *html.Node, links *[]Link) { + if n.Type == html.ElementNode && n.Data == "link" { + link := parseLink(n) + if link != nil { + *links = append(*links, *link) + } + } + + for c := n.FirstChild; c != nil; c = c.NextSibling { + findLinks(c, links) + } +} + +func parseLink(n *html.Node) *Link { + var link Link + + for _, attr := range n.Attr { + switch attr.Key { + case "rel": + link.Rel = attr.Val + case "sizes": + link.Sizes = attr.Val + case "href": + link.Href = attr.Val + } + } + + if link.Href != "" { + return &link + } + + return nil +} + +func chooseFaviconLink(links []Link) *Link { + var appleTouchLinks []Link + var shortcutLinks []Link + var iconLinks []Link + + for _, link := range links { + switch link.Rel { + case "apple-touch-icon": + appleTouchLinks = append(appleTouchLinks, link) + case "shortcut icon": + shortcutLinks = append(shortcutLinks, link) + case "icon": + iconLinks = append(iconLinks, link) + } + } + + if len(appleTouchLinks) > 0 { + return chooseFaviconLinkBySizes(appleTouchLinks) + } + + if len(shortcutLinks) > 0 { + return chooseFaviconLinkBySizes(shortcutLinks) + } + + if len(iconLinks) > 0 { + return chooseFaviconLinkBySizes(iconLinks) + } + + return nil +} + +func chooseFaviconLinkBySizes(links []Link) *Link { + if len(links) == 1 { + return &links[0] + } + + var chosenLink *Link + + for _, link := range links { + if chosenLink == nil || compareSizes(link.Sizes, chosenLink.Sizes) > 0 { + chosenLink = &link + } + } + + return chosenLink +} + +func compareSizes(sizes1, sizes2 string) int { + if sizes1 == sizes2 { + return 0 + } + + size1 := parseSize(sizes1) + size2 := parseSize(sizes2) + + if size1 == nil { + return -1 + } + + if size2 == nil { + return 1 + } + + if size1[0] == size2[0] { + return size1[1] - size2[1] + } + + return size1[0] - size2[0] +} + +func parseSize(sizes string) []int { + size := strings.Split(sizes, "x") + if len(size) != 2 { + return nil + } + + var result []int + + for _, s := range size { + val := strings.TrimSpace(s) + if len(val) > 0 { + num := 0 + for i := 0; i < len(val); i++ { + if val[i] >= '0' && val[i] <= '9' { + num = num*10 + int(val[i]-'0') + } else { + break + } + } + result = append(result, num) + } + } + + if len(result) == 2 { + return result + } + + return nil +} + func getFaviconFileBuffer(client *http.Client, email string) (*bytes.Buffer, string, error) { tokens := strings.Split(email, "@") domain := tokens[1] @@ -28,9 +192,29 @@ func getFaviconFileBuffer(client *http.Client, email string) (*bytes.Buffer, str return nil, "", nil } - //htmlUrl := fmt.Sprintf("https://%s", domain) - //buffer, fileExtension, err := downloadImage(client, htmlUrl) + htmlUrl := fmt.Sprintf("https://%s", domain) + buffer, _, err := downloadImage(client, htmlUrl) + if err != nil { + return nil, "", err + } - faviconUrl := fmt.Sprintf("https://%s/favicon.ico", domain) + faviconUrl := "" + if buffer != nil { + faviconUrl, err = GetFaviconUrl(buffer.String()) + if err != nil { + return nil, "", err + } + + if !strings.HasPrefix(faviconUrl, "http") { + faviconUrl, err = url.JoinPath(htmlUrl, faviconUrl) + if err != nil { + return nil, "", err + } + } + } + + if faviconUrl == "" { + faviconUrl = fmt.Sprintf("https://%s/favicon.ico", domain) + } return downloadImage(client, faviconUrl) } diff --git a/object/user_avatar_test.go b/object/user_avatar_test.go index 23b14eb8..08f2e56e 100644 --- a/object/user_avatar_test.go +++ b/object/user_avatar_test.go @@ -61,7 +61,7 @@ func TestUpdateAvatars(t *testing.T) { // continue //} - if user.AvatarType != "Favicon" { + if user.AvatarType != "Auto" { continue }