diff --git a/go.mod b/go.mod index f1aadebd..df8711a4 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/casdoor/xorm-adapter/v3 v3.0.4 github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f github.com/denisenkom/go-mssqldb v0.9.0 + github.com/fogleman/gg v1.3.0 github.com/forestmgy/ldapserver v1.1.0 github.com/go-git/go-git/v5 v5.6.0 github.com/go-ldap/ldap/v3 v3.3.0 @@ -25,6 +26,7 @@ require ( github.com/go-sql-driver/mysql v1.6.0 github.com/go-webauthn/webauthn v0.6.0 github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/uuid v1.3.0 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect diff --git a/go.sum b/go.sum index 8ac02d61..46ae8c5c 100644 --- a/go.sum +++ b/go.sum @@ -173,6 +173,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/forestmgy/ldapserver v1.1.0 h1:gvil4nuLhqPEL8SugCkFhRyA0/lIvRdwZSqlrw63ll4= github.com/forestmgy/ldapserver v1.1.0/go.mod h1:1RZ8lox1QSY7rmbjdmy+sYQXY4Lp7SpGzpdE3+j3IyM= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= @@ -239,6 +241,8 @@ github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOW github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -677,6 +681,7 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= diff --git a/object/avatar.go b/object/avatar.go index c925239f..387b63e8 100644 --- a/object/avatar.go +++ b/object/avatar.go @@ -80,3 +80,21 @@ func DownloadAndUpload(url string, fullFilePath string, lang string) { panic(err) } } + +func getPermanentAvatarUrlFromBuffer(organization string, username string, fileBuffer *bytes.Buffer, ext string, upload bool) string { + if defaultStorageProvider == nil { + return "" + } + + fullFilePath := fmt.Sprintf("/avatar/%s/%s%s", organization, username, ext) + uploadedFileUrl, _ := GetUploadFileUrl(defaultStorageProvider, fullFilePath, false) + + if upload { + _, _, err := UploadFileSafe(defaultStorageProvider, fullFilePath, fileBuffer, "en") + if err != nil { + panic(err) + } + } + + return uploadedFileUrl +} diff --git a/object/avatar_test.go b/object/avatar_test.go index fa03a8c3..7827c011 100644 --- a/object/avatar_test.go +++ b/object/avatar_test.go @@ -16,6 +16,7 @@ package object import ( "fmt" + "strings" "testing" "github.com/casdoor/casdoor/proxy" @@ -37,3 +38,22 @@ func TestSyncPermanentAvatars(t *testing.T) { fmt.Printf("[%d/%d]: Update user: [%s]'s permanent avatar: %s\n", i, len(users), user.GetId(), user.PermanentAvatar) } } + +func TestUpdateAvatars(t *testing.T) { + InitConfig() + InitDefaultStorageProvider() + proxy.InitHttpClient() + + users := GetUsers("casdoor") + for _, user := range users { + if strings.HasPrefix(user.Avatar, "http") { + continue + } + + updated := user.refreshAvatar() + if updated { + user.PermanentAvatar = "*" + UpdateUser(user.GetId(), user, []string{"avatar"}, true) + } + } +} diff --git a/object/avatar_util.go b/object/avatar_util.go new file mode 100644 index 00000000..56da3a8c --- /dev/null +++ b/object/avatar_util.go @@ -0,0 +1,167 @@ +// 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/product_test.go b/object/product_test.go index 9123ac67..682777ca 100644 --- a/object/product_test.go +++ b/object/product_test.go @@ -30,7 +30,10 @@ func TestProduct(t *testing.T) { product := GetProduct("admin/product_123") provider := getProvider(product.Owner, "provider_pay_alipay") cert := getCert(product.Owner, "cert-pay-alipay") - pProvider := pp.GetPaymentProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, cert.Certificate, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey, provider.ClientId2) + pProvider, err := pp.GetPaymentProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, cert.Certificate, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey, provider.ClientId2) + if err != nil { + panic(err) + } paymentName := util.GenerateTimeId() returnUrl := "" diff --git a/object/user.go b/object/user.go index c2417897..aac8e3c7 100644 --- a/object/user.go +++ b/object/user.go @@ -15,10 +15,12 @@ package object import ( + "bytes" "fmt" "strings" "github.com/casdoor/casdoor/conf" + "github.com/casdoor/casdoor/proxy" "github.com/casdoor/casdoor/util" "github.com/go-webauthn/webauthn/webauthn" "github.com/xorm-io/core" @@ -693,3 +695,40 @@ func userChangeTrigger(oldName string, newName string) error { return session.Commit() } + +func (user *User) refreshAvatar() bool { + var err error + var fileBuffer *bytes.Buffer + var ext string + + // Gravatar + Identicon + if strings.Contains(user.Avatar, "Gravatar") && user.Email != "" { + client := proxy.ProxyHttpClient + has, err := hasGravatar(client, user.Email) + if err != nil { + panic(err) + } + + if has { + fileBuffer, ext, err = getGravatarFileBuffer(client, user.Email) + if err != nil { + panic(err) + } + } + } + + if fileBuffer == nil && strings.Contains(user.Avatar, "Identicon") { + fileBuffer, ext, err = getIdenticonFileBuffer(user.Name) + if err != nil { + panic(err) + } + } + + if fileBuffer != nil { + avatarUrl := getPermanentAvatarUrlFromBuffer(user.Owner, user.Name, fileBuffer, ext, true) + user.Avatar = avatarUrl + return true + } + + return false +}