Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Direct avatar rendering #13649

Merged
merged 23 commits into from
Dec 3, 2020
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions models/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,6 @@ func (a *Action) GetDisplayNameTitle() string {
return a.GetActFullName()
}

// GetActAvatar the action's user's avatar link
func (a *Action) GetActAvatar() string {
a.loadActUser()
return a.ActUser.RelAvatarLink()
}

// GetRepoUserName returns the name of the action repository owner.
func (a *Action) GetRepoUserName() string {
a.loadRepo()
Expand Down
70 changes: 68 additions & 2 deletions models/avatar.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import (
"crypto/md5"
"fmt"
"net/url"
"path"
"strconv"
"strings"

"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)

Expand All @@ -20,6 +24,21 @@ type EmailHash struct {
Email string `xorm:"UNIQUE NOT NULL"`
}

// DefaultAvatarLink the default avatar link
func DefaultAvatarLink() string {
return setting.AppSubURL + "/img/avatar_default.png"
}

// DefaultAvatarSize is a sentinel value for the default avatar size, as
// determined by the avatar-hosting service.
const DefaultAvatarSize = -1

// HashEmail hashes email address to MD5 string.
// https://en.gravatar.com/site/implement/hash/
func HashEmail(email string) string {
return base.EncodeMD5(strings.ToLower(strings.TrimSpace(email)))
}

// GetEmailForHash converts a provided md5sum to the email
func GetEmailForHash(md5Sum string) (string, error) {
return cache.GetString("Avatar:"+md5Sum, func() (string, error) {
Expand All @@ -32,8 +51,24 @@ func GetEmailForHash(md5Sum string) (string, error) {
})
}

// AvatarLink returns an avatar link for a provided email
func AvatarLink(email string) string {
// LibravatarURL returns the URL for the given email. This function should only
// be called if a federated avatar service is enabled.
func LibravatarURL(email string) (*url.URL, error) {
urlStr, err := setting.LibravatarService.FromEmail(email)
if err != nil {
log.Error("LibravatarService.FromEmail(email=%s): error %v", email, err)
return nil, err
}
u, err := url.Parse(urlStr)
if err != nil {
log.Error("Failed to parse libravatar url(%s): error %v", urlStr, err)
return nil, err
}
return u, nil
}

// HashedAvatarLink returns an avatar link for a provided email
func HashedAvatarLink(email string) string {
lowerEmail := strings.ToLower(strings.TrimSpace(email))
sum := fmt.Sprintf("%x", md5.Sum([]byte(lowerEmail)))
_, _ = cache.GetString("Avatar:"+sum, func() (string, error) {
Expand All @@ -57,3 +92,34 @@ func AvatarLink(email string) string {
})
return setting.AppSubURL + "/avatar/" + url.PathEscape(sum)
}

// MakeFinalAvatarURL constructs the final avatar URL string
func MakeFinalAvatarURL(u *url.URL, size int) string {
vals := u.Query()
vals.Set("d", "identicon")
if size != DefaultAvatarSize {
vals.Set("s", strconv.Itoa(size))
}
u.RawQuery = vals.Encode()
return u.String()
}

// SizedAvatarLink returns a sized link to the avatar for the given email address.
func SizedAvatarLink(email string, size int) string {
var avatarURL *url.URL
if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
// This is the slow path that would need to call LibravatarURL() which
// does DNS lookups. Avoid it by issuing a redirect so we don't block
// the template render with network requests.
return HashedAvatarLink(email)
} else if !setting.DisableGravatar {
// copy GravatarSourceURL, because we will modify its Path.
copyOfGravatarSourceURL := *setting.GravatarSourceURL
avatarURL = &copyOfGravatarSourceURL
avatarURL.Path = path.Join(avatarURL.Path, HashEmail(email))
} else {
return DefaultAvatarLink()
}

return MakeFinalAvatarURL(avatarURL, size)
}
52 changes: 52 additions & 0 deletions models/avatar_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package models

import (
"net/url"
"testing"

"code.gitea.io/gitea/modules/setting"

"github.com/stretchr/testify/assert"
)

const gravatarSource = "https://secure.gravatar.com/avatar/"

func disableGravatar() {
setting.EnableFederatedAvatar = false
setting.LibravatarService = nil
setting.DisableGravatar = true
}

func enableGravatar(t *testing.T) {
setting.DisableGravatar = false
var err error
setting.GravatarSourceURL, err = url.Parse(gravatarSource)
assert.NoError(t, err)
}

func TestHashEmail(t *testing.T) {
assert.Equal(t,
"d41d8cd98f00b204e9800998ecf8427e",
HashEmail(""),
)
assert.Equal(t,
"353cbad9b58e69c96154ad99f92bedc7",
HashEmail("gitea@example.com"),
)
}

func TestSizedAvatarLink(t *testing.T) {
disableGravatar()
assert.Equal(t, "/img/avatar_default.png",
SizedAvatarLink("gitea@example.com", 100))

enableGravatar(t)
assert.Equal(t,
"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100",
SizedAvatarLink("gitea@example.com", 100),
)
}
4 changes: 2 additions & 2 deletions models/user_avatar.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (u *User) generateRandomAvatar(e Engine) error {
}

if u.Avatar == "" {
u.Avatar = base.HashEmail(u.AvatarEmail)
u.Avatar = HashEmail(u.AvatarEmail)
}

if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
Expand Down Expand Up @@ -94,7 +94,7 @@ func (u *User) RealSizedAvatarLink(size int) string {

return setting.AppSubURL + "/avatars/" + u.Avatar
}
return base.SizedAvatarLink(u.AvatarEmail, size)
return SizedAvatarLink(u.AvatarEmail, size)
}

// RelAvatarLink returns a relative link to the user's avatar. The link
Expand Down
80 changes: 0 additions & 80 deletions modules/base/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ import (
"encoding/hex"
"fmt"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"runtime"
"strconv"
Expand Down Expand Up @@ -134,12 +132,6 @@ func CreateTimeLimitCode(data string, minutes int, startInf interface{}) string
return code
}

// HashEmail hashes email address to MD5 string.
// https://en.gravatar.com/site/implement/hash/
func HashEmail(email string) string {
return EncodeMD5(strings.ToLower(strings.TrimSpace(email)))
}

// DefaultAvatarLink the default avatar link
func DefaultAvatarLink() string {
return setting.AppSubURL + "/img/avatar_default.png"
Expand All @@ -149,78 +141,6 @@ func DefaultAvatarLink() string {
// determined by the avatar-hosting service.
const DefaultAvatarSize = -1

// libravatarURL returns the URL for the given email. This function should only
// be called if a federated avatar service is enabled.
func libravatarURL(email string) (*url.URL, error) {
urlStr, err := setting.LibravatarService.FromEmail(email)
if err != nil {
log.Error("LibravatarService.FromEmail(email=%s): error %v", email, err)
return nil, err
}
u, err := url.Parse(urlStr)
if err != nil {
log.Error("Failed to parse libravatar url(%s): error %v", urlStr, err)
return nil, err
}
return u, nil
}

// SizedAvatarLink returns a sized link to the avatar for the given email
// address.
func SizedAvatarLink(email string, size int) string {
var avatarURL *url.URL
if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
var err error
avatarURL, err = libravatarURL(email)
if err != nil {
return DefaultAvatarLink()
}
} else if !setting.DisableGravatar {
// copy GravatarSourceURL, because we will modify its Path.
copyOfGravatarSourceURL := *setting.GravatarSourceURL
avatarURL = &copyOfGravatarSourceURL
avatarURL.Path = path.Join(avatarURL.Path, HashEmail(email))
} else {
return DefaultAvatarLink()
}

vals := avatarURL.Query()
vals.Set("d", "identicon")
if size != DefaultAvatarSize {
vals.Set("s", strconv.Itoa(size))
}
avatarURL.RawQuery = vals.Encode()
return avatarURL.String()
}

// SizedAvatarLinkWithDomain returns a sized link to the avatar for the given email
// address.
func SizedAvatarLinkWithDomain(email string, size int) string {
var avatarURL *url.URL
if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
var err error
avatarURL, err = libravatarURL(email)
if err != nil {
return DefaultAvatarLink()
}
} else if !setting.DisableGravatar {
// copy GravatarSourceURL, because we will modify its Path.
copyOfGravatarSourceURL := *setting.GravatarSourceURL
avatarURL = &copyOfGravatarSourceURL
avatarURL.Path = path.Join(avatarURL.Path, HashEmail(email))
} else {
return DefaultAvatarLink()
}

vals := avatarURL.Query()
vals.Set("d", "identicon")
if size != DefaultAvatarSize {
vals.Set("s", strconv.Itoa(size))
}
avatarURL.RawQuery = vals.Encode()
return avatarURL.String()
}

// FileSize calculates the file size and generate user-friendly string.
func FileSize(s int64) string {
return humanize.IBytes(uint64(s))
Expand Down
41 changes: 0 additions & 41 deletions modules/base/tool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@
package base

import (
"net/url"
"testing"

"code.gitea.io/gitea/modules/setting"

"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -56,44 +53,6 @@ func TestBasicAuthEncode(t *testing.T) {
// TODO: Test VerifyTimeLimitCode()
// TODO: Test CreateTimeLimitCode()

func TestHashEmail(t *testing.T) {
assert.Equal(t,
"d41d8cd98f00b204e9800998ecf8427e",
HashEmail(""),
)
assert.Equal(t,
"353cbad9b58e69c96154ad99f92bedc7",
HashEmail("gitea@example.com"),
)
}

const gravatarSource = "https://secure.gravatar.com/avatar/"

func disableGravatar() {
setting.EnableFederatedAvatar = false
setting.LibravatarService = nil
setting.DisableGravatar = true
}

func enableGravatar(t *testing.T) {
setting.DisableGravatar = false
var err error
setting.GravatarSourceURL, err = url.Parse(gravatarSource)
assert.NoError(t, err)
}

func TestSizedAvatarLink(t *testing.T) {
disableGravatar()
assert.Equal(t, "/img/avatar_default.png",
SizedAvatarLink("gitea@example.com", 100))

enableGravatar(t)
assert.Equal(t,
"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100",
SizedAvatarLink("gitea@example.com", 100),
)
}

func TestFileSize(t *testing.T) {
var size int64 = 512
assert.Equal(t, "512 B", FileSize(size))
Expand Down
2 changes: 1 addition & 1 deletion modules/repository/commits.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func (pc *PushCommits) AvatarLink(email string) string {
var err error
u, err = models.GetUserByEmail(email)
if err != nil {
pc.avatars[email] = models.AvatarLink(email)
pc.avatars[email] = models.HashedAvatarLink(email)
if !models.IsErrUserNotExist(err) {
log.Error("GetUserByEmail: %v", err)
return ""
Expand Down
Loading