diff --git a/object/avatar_util.go b/object/avatar_util.go deleted file mode 100644 index 56da3a8c..00000000 --- a/object/avatar_util.go +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2023 The Casdoor Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package object - -import ( - "bytes" - "crypto/md5" - "fmt" - "image" - "image/color" - "image/png" - "io" - "net/http" - "strings" - - "github.com/fogleman/gg" -) - -func hasGravatar(client *http.Client, email string) (bool, error) { - // Clean and lowercase the email - email = strings.TrimSpace(strings.ToLower(email)) - - // Generate MD5 hash of the email - hash := md5.New() - io.WriteString(hash, email) - hashedEmail := fmt.Sprintf("%x", hash.Sum(nil)) - - // Create Gravatar URL with d=404 parameter - gravatarURL := fmt.Sprintf("https://www.gravatar.com/avatar/%s?d=404", hashedEmail) - - // Send a request to Gravatar - req, err := http.NewRequest("GET", gravatarURL, nil) - if err != nil { - return false, err - } - - resp, err := client.Do(req) - if err != nil { - return false, err - } - defer resp.Body.Close() - - // Check if the user has a custom Gravatar image - if resp.StatusCode == http.StatusOK { - return true, nil - } else if resp.StatusCode == http.StatusNotFound { - return false, nil - } else { - return false, fmt.Errorf("failed to fetch gravatar image: %s", resp.Status) - } -} - -func getGravatarFileBuffer(client *http.Client, email string) (*bytes.Buffer, string, error) { - // Clean and lowercase the email - email = strings.TrimSpace(strings.ToLower(email)) - - // Generate MD5 hash of the email - hash := md5.New() - io.WriteString(hash, email) - hashedEmail := fmt.Sprintf("%x", hash.Sum(nil)) - - // Create Gravatar URL - gravatarURL := fmt.Sprintf("https://www.gravatar.com/avatar/%s", hashedEmail) - - // Download the image - req, err := http.NewRequest("GET", gravatarURL, nil) - if err != nil { - return nil, "", err - } - - resp, err := client.Do(req) - if err != nil { - return nil, "", err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, "", fmt.Errorf("failed to download gravatar image: %s", resp.Status) - } - - // 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" - default: - return nil, "", fmt.Errorf("unsupported content type: %s", contentType) - } - - // Save the image to a bytes.Buffer - buffer := &bytes.Buffer{} - _, err = io.Copy(buffer, resp.Body) - if err != nil { - return nil, "", err - } - - return buffer, fileExtension, nil -} - -func getColor(data []byte) color.RGBA { - r := int(data[0]) % 256 - g := int(data[1]) % 256 - b := int(data[2]) % 256 - return color.RGBA{uint8(r), uint8(g), uint8(b), 255} -} - -func getIdenticonFileBuffer(username string) (*bytes.Buffer, string, error) { - username = strings.TrimSpace(strings.ToLower(username)) - - hash := md5.New() - io.WriteString(hash, username) - hashedUsername := hash.Sum(nil) - - // Define the size of the image - const imageSize = 420 - const cellSize = imageSize / 7 - - // Create a new image - img := image.NewRGBA(image.Rect(0, 0, imageSize, imageSize)) - - // Create a context - dc := gg.NewContextForRGBA(img) - - // Set a background color - dc.SetColor(color.RGBA{240, 240, 240, 255}) - dc.Clear() - - // Get avatar color - avatarColor := getColor(hashedUsername) - - // Draw cells - for i := 0; i < 7; i++ { - for j := 0; j < 7; j++ { - if (hashedUsername[i] >> uint(j) & 1) == 1 { - dc.SetColor(avatarColor) - dc.DrawRectangle(float64(j*cellSize), float64(i*cellSize), float64(cellSize), float64(cellSize)) - dc.Fill() - } - } - } - - // Save image to a bytes.Buffer - buffer := &bytes.Buffer{} - err := png.Encode(buffer, img) - if err != nil { - return nil, "", fmt.Errorf("failed to save image: %w", err) - } - - return buffer, ".png", nil -} diff --git a/object/user.go b/object/user.go index 03366714..39de7d33 100644 --- a/object/user.go +++ b/object/user.go @@ -44,6 +44,7 @@ type User struct { FirstName string `xorm:"varchar(100)" json:"firstName"` LastName string `xorm:"varchar(100)" json:"lastName"` Avatar string `xorm:"varchar(500)" json:"avatar"` + AvatarType string `xorm:"varchar(100)" json:"avatarType"` PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"` Email string `xorm:"varchar(100) index" json:"email"` EmailVerified bool `json:"emailVerified"` diff --git a/object/user_avatar.go b/object/user_avatar.go index 36c0106f..6d5ccb0c 100644 --- a/object/user_avatar.go +++ b/object/user_avatar.go @@ -16,19 +16,78 @@ package object import ( "bytes" + "fmt" + "io" + "net/http" "strings" "github.com/casdoor/casdoor/proxy" ) +func downloadImage(client *http.Client, url string) (*bytes.Buffer, string, error) { + // Download the image + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, "", err + } + + resp, err := client.Do(req) + if err != nil { + fmt.Printf("downloadImage() error for url [%s]: %s\n", url, err.Error()) + if strings.Contains(err.Error(), "EOF") { + return nil, "", nil + } else { + return nil, "", err + } + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + fmt.Printf("downloadImage() error for url [%s]: %s\n", url, resp.Status) + if resp.StatusCode == 404 { + return nil, "", nil + } else { + return nil, "", fmt.Errorf("failed to download gravatar image: %s", resp.Status) + } + } + + // 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) + } + + // Save the image to a bytes.Buffer + buffer := &bytes.Buffer{} + _, err = io.Copy(buffer, resp.Body) + if err != nil { + return nil, "", err + } + + return buffer, fileExtension, nil +} + func (user *User) refreshAvatar() (bool, error) { var err error var fileBuffer *bytes.Buffer var ext string - // Gravatar + Identicon - if strings.Contains(user.Avatar, "Gravatar") && user.Email != "" { + // Gravatar + if (user.AvatarType == "Auto" || user.AvatarType == "Gravatar") && user.Email != "" { client := proxy.ProxyHttpClient + has, err := hasGravatar(client, user.Email) if err != nil { return false, err @@ -39,14 +98,37 @@ func (user *User) refreshAvatar() (bool, error) { if err != nil { return false, err } + + if fileBuffer != nil { + user.AvatarType = "Gravatar" + } } } - if fileBuffer == nil && strings.Contains(user.Avatar, "Identicon") { + // Favicon + if fileBuffer == nil && (user.AvatarType == "Auto" || user.AvatarType == "Favicon") { + client := proxy.ProxyHttpClient + + fileBuffer, ext, err = getFaviconFileBuffer(client, user.Email) + if err != nil { + return false, err + } + + if fileBuffer != nil { + user.AvatarType = "Favicon" + } + } + + // Identicon + if fileBuffer == nil && (user.AvatarType == "Auto" || user.AvatarType == "Identicon") { fileBuffer, ext, err = getIdenticonFileBuffer(user.Name) if err != nil { return false, err } + + if fileBuffer != nil { + user.AvatarType = "Identicon" + } } if fileBuffer != nil { diff --git a/object/user_avatar_favicon.go b/object/user_avatar_favicon.go new file mode 100644 index 00000000..b76e0207 --- /dev/null +++ b/object/user_avatar_favicon.go @@ -0,0 +1,36 @@ +// Copyright 2023 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "bytes" + "fmt" + "net/http" + "strings" +) + +func getFaviconFileBuffer(client *http.Client, email string) (*bytes.Buffer, string, error) { + tokens := strings.Split(email, "@") + domain := tokens[1] + if domain == "gmail.com" || domain == "163.com" || domain == "qq.com" { + return nil, "", nil + } + + //htmlUrl := fmt.Sprintf("https://%s", domain) + //buffer, fileExtension, err := downloadImage(client, htmlUrl) + + faviconUrl := fmt.Sprintf("https://%s/favicon.ico", domain) + return downloadImage(client, faviconUrl) +} diff --git a/object/user_avatar_gravatar.go b/object/user_avatar_gravatar.go new file mode 100644 index 00000000..c759696e --- /dev/null +++ b/object/user_avatar_gravatar.go @@ -0,0 +1,76 @@ +// Copyright 2023 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "bytes" + "crypto/md5" + "fmt" + "io" + "net/http" + "strings" +) + +func hasGravatar(client *http.Client, email string) (bool, error) { + // Clean and lowercase the email + email = strings.TrimSpace(strings.ToLower(email)) + + // Generate MD5 hash of the email + hash := md5.New() + io.WriteString(hash, email) + hashedEmail := fmt.Sprintf("%x", hash.Sum(nil)) + + // Create Gravatar URL with d=404 parameter + gravatarURL := fmt.Sprintf("https://www.gravatar.com/avatar/%s?d=404", hashedEmail) + + // Send a request to Gravatar + req, err := http.NewRequest("GET", gravatarURL, nil) + if err != nil { + return false, err + } + + resp, err := client.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + // Check if the user has a custom Gravatar image + if resp.StatusCode == http.StatusOK { + return true, nil + } else if resp.StatusCode == http.StatusNotFound { + return false, nil + } else { + return false, fmt.Errorf("failed to fetch gravatar image: %s", resp.Status) + } +} + +func getGravatarFileBuffer(client *http.Client, email string) (*bytes.Buffer, string, error) { + // Clean and lowercase the email + email = strings.TrimSpace(strings.ToLower(email)) + + // Generate MD5 hash of the email + hash := md5.New() + _, err := io.WriteString(hash, email) + if err != nil { + return nil, "", err + } + hashedEmail := fmt.Sprintf("%x", hash.Sum(nil)) + + // Create Gravatar URL + gravatarUrl := fmt.Sprintf("https://www.gravatar.com/avatar/%s", hashedEmail) + + return downloadImage(client, gravatarUrl) +} diff --git a/object/user_avatar_identicon.go b/object/user_avatar_identicon.go new file mode 100644 index 00000000..c3de4c41 --- /dev/null +++ b/object/user_avatar_identicon.go @@ -0,0 +1,80 @@ +// Copyright 2023 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "bytes" + "crypto/md5" + "fmt" + "image" + "image/color" + "image/png" + "io" + "strings" + + "github.com/fogleman/gg" +) + +func getColor(data []byte) color.RGBA { + r := int(data[0]) % 256 + g := int(data[1]) % 256 + b := int(data[2]) % 256 + return color.RGBA{uint8(r), uint8(g), uint8(b), 255} +} + +func getIdenticonFileBuffer(username string) (*bytes.Buffer, string, error) { + username = strings.TrimSpace(strings.ToLower(username)) + + hash := md5.New() + io.WriteString(hash, username) + hashedUsername := hash.Sum(nil) + + // Define the size of the image + const imageSize = 420 + const cellSize = imageSize / 7 + + // Create a new image + img := image.NewRGBA(image.Rect(0, 0, imageSize, imageSize)) + + // Create a context + dc := gg.NewContextForRGBA(img) + + // Set a background color + dc.SetColor(color.RGBA{240, 240, 240, 255}) + dc.Clear() + + // Get avatar color + avatarColor := getColor(hashedUsername) + + // Draw cells + for i := 0; i < 7; i++ { + for j := 0; j < 7; j++ { + if (hashedUsername[i] >> uint(j) & 1) == 1 { + dc.SetColor(avatarColor) + dc.DrawRectangle(float64(j*cellSize), float64(i*cellSize), float64(cellSize), float64(cellSize)) + dc.Fill() + } + } + } + + // Save image to a bytes.Buffer + buffer := &bytes.Buffer{} + err := png.Encode(buffer, img) + if err != nil { + return nil, "", fmt.Errorf("failed to save image: %w", err) + } + + return buffer, ".png", nil +} diff --git a/object/user_avatar_test.go b/object/user_avatar_test.go index d5dcbf2d..23b14eb8 100644 --- a/object/user_avatar_test.go +++ b/object/user_avatar_test.go @@ -16,7 +16,6 @@ package object import ( "fmt" - "strings" "testing" "github.com/casdoor/casdoor/proxy" @@ -58,7 +57,11 @@ func TestUpdateAvatars(t *testing.T) { } for _, user := range users { - if strings.HasPrefix(user.Avatar, "http") { + //if strings.HasPrefix(user.Avatar, "http") { + // continue + //} + + if user.AvatarType != "Favicon" { continue } @@ -69,7 +72,7 @@ func TestUpdateAvatars(t *testing.T) { if updated { user.PermanentAvatar = "*" - _, err = UpdateUser(user.GetId(), user, []string{"avatar"}, true) + _, err = UpdateUser(user.GetId(), user, []string{"avatar", "avatar_type"}, true) if err != nil { panic(err) } diff --git a/proxy/proxy.go b/proxy/proxy.go index 9069f8cc..2f70ec6e 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -15,6 +15,7 @@ package proxy import ( + "crypto/tls" "fmt" "net" "net/http" @@ -71,7 +72,7 @@ func getProxyHttpClient() *http.Client { panic(err) } - tr := &http.Transport{Dial: dialer.Dial} + tr := &http.Transport{Dial: dialer.Dial, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} return &http.Client{ Transport: tr, }