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

make avatar lookup occur at image request #10540

Merged
merged 17 commits into from
Mar 27, 2020
Merged
Show file tree
Hide file tree
Changes from 10 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
48 changes: 48 additions & 0 deletions models/avatar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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 (
"crypto/md5"
"fmt"
"net/url"
"strings"

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

// EmailHash represents a pre-generated hash map
type EmailHash struct {
Hash string `xorm:"pk hash UNIQUE NOT NULL"`
zeripath marked this conversation as resolved.
Show resolved Hide resolved
Email string `xorm:"UNIQUE NOT NULL"`
}

// GetEmailForHash converts a provided md5sum to the email
func GetEmailForHash(md5Sum string) (string, error) {
return cache.GetString("Avatar:"+md5Sum, func() (string, error) {
emailHash := EmailHash{
Hash: strings.ToLower(strings.TrimSpace(md5Sum)),
}

_, err := x.Get(&emailHash)
return emailHash.Email, err
})
}

// AvatarLink returns an avatar link for a provided email
func AvatarLink(email string) string {
lowerEmail := strings.ToLower(strings.TrimSpace(email))
sum := fmt.Sprintf("%x", md5.Sum([]byte(lowerEmail)))
_, _ = cache.GetString("Avatar:"+sum, func() (string, error) {
emailHash := &EmailHash{
Email: lowerEmail,
Hash: sum,
}
_, _ = x.Insert(emailHash)
return lowerEmail, nil
})
return setting.AppSubURL + "/avatar/" + url.PathEscape(sum)
}
1 change: 1 addition & 0 deletions models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ func init() {
new(OAuth2Grant),
new(Task),
new(LanguageStat),
new(EmailHash),
)

gonicNames := []string{"SSL", "UID"}
Expand Down
31 changes: 26 additions & 5 deletions modules/base/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,32 @@ func SizedAvatarLink(email string, size int) string {
return avatarURL.String()
}

// AvatarLink returns relative avatar link to the site domain by given email,
// which includes app sub-url as prefix. However, it is possible
// to return full URL if user enables Gravatar-like service.
func AvatarLink(email string) string {
return SizedAvatarLink(email, DefaultAvatarSize)
// 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.
Expand Down
11 changes: 0 additions & 11 deletions modules/base/tool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,6 @@ func TestSizedAvatarLink(t *testing.T) {
)
}

func TestAvatarLink(t *testing.T) {
disableGravatar()
assert.Equal(t, "/img/avatar_default.png", AvatarLink("[email protected]"))

enableGravatar(t)
assert.Equal(t,
"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon",
AvatarLink("[email protected]"),
)
}

func TestFileSize(t *testing.T) {
var size int64 = 512
assert.Equal(t, "512 B", FileSize(size))
Expand Down
28 changes: 28 additions & 0 deletions modules/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,34 @@ func NewContext() error {
return err
}

// GetString returns the key value from cache with callback when no key exists in cache
func GetString(key string, getFunc func() (string, error)) (string, error) {
if conn == nil || setting.CacheService.TTL == 0 {
return getFunc()
}
if !conn.IsExist(key) {
var (
value string
err error
)
if value, err = getFunc(); err != nil {
return value, err
}
err = conn.Put(key, value, int64(setting.CacheService.TTL.Seconds()))
if err != nil {
return "", err
}
}
value := conn.Get(key)
if v, ok := value.(string); ok {
return v, nil
}
if v, ok := value.(fmt.Stringer); ok {
return v.String(), nil
}
return fmt.Sprintf("%s", conn.Get(key)), nil
}

// GetInt returns key value from cache with callback when no key exists in cache
func GetInt(key string, getFunc func() (int, error)) (int, error) {
if conn == nil || setting.CacheService.TTL == 0 {
Expand Down
3 changes: 1 addition & 2 deletions modules/repository/commits.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"time"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
Expand Down Expand Up @@ -124,7 +123,7 @@ func (pc *PushCommits) AvatarLink(email string) string {
var err error
u, err = models.GetUserByEmail(email)
if err != nil {
pc.avatars[email] = base.AvatarLink(email)
pc.avatars[email] = models.AvatarLink(email)
if !models.IsErrUserNotExist(err) {
log.Error("GetUserByEmail: %v", err)
return ""
Expand Down
4 changes: 3 additions & 1 deletion modules/repository/commits_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package repository

import (
"container/list"
"crypto/md5"
"fmt"
"testing"
"time"

Expand Down Expand Up @@ -114,7 +116,7 @@ func TestPushCommits_AvatarLink(t *testing.T) {
pushCommits.AvatarLink("[email protected]"))

assert.Equal(t,
"https://secure.gravatar.com/avatar/19ade630b94e1e0535b3df7387434154?d=identicon",
"/avatar/"+fmt.Sprintf("%x", md5.Sum([]byte("nonexistent@example.com"))),
pushCommits.AvatarLink("[email protected]"))
}

Expand Down
2 changes: 1 addition & 1 deletion modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func NewFuncMap() []template.FuncMap {
"AllowedReactions": func() []string {
return setting.UI.Reactions
},
"AvatarLink": base.AvatarLink,
"AvatarLink": models.AvatarLink,
"Safe": Safe,
"SafeJS": SafeJS,
"Str2html": Str2html,
Expand Down
2 changes: 1 addition & 1 deletion routers/repo/blame.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
}
avatar = fmt.Sprintf(`<a href="%s/%s"><img class="ui avatar image" src="%s" title="%s" alt=""/></a>`, setting.AppSubURL, url.PathEscape(commit.User.Name), commit.User.RelAvatarLink(), html.EscapeString(authorName))
} else {
avatar = fmt.Sprintf(`<img class="ui avatar image" src="%s" title="%s"/>`, html.EscapeString(base.AvatarLink(commit.Author.Email)), html.EscapeString(commit.Author.Name))
avatar = fmt.Sprintf(`<img class="ui avatar image" src="%s" title="%s"/>`, html.EscapeString(models.AvatarLink(commit.Author.Email)), html.EscapeString(commit.Author.Name))
}
commitInfo.WriteString(fmt.Sprintf(`<div class="blame-info%s"><div class="blame-data"><div class="blame-avatar">%s</div><div class="blame-message"><a href="%s/commit/%s" title="%[5]s">%[5]s</a></div><div class="blame-time">%s</div></div></div>`, attr, avatar, repoLink, part.Sha, html.EscapeString(commit.CommitMessage), commitSince))
} else {
Expand Down
2 changes: 2 additions & 0 deletions routers/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,8 @@ func RegisterRoutes(m *macaron.Macaron) {
})
// ***** END: User *****

m.Get("/avatar/:hash", user.AvatarByEmailHash)

adminReq := context.Toggle(&context.ToggleOptions{SignInRequired: true, AdminRequired: true})

// ***** START: Admin *****
Expand Down
25 changes: 25 additions & 0 deletions routers/user/avatar.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
package user

import (
"errors"
"strconv"
"strings"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
)
Expand Down Expand Up @@ -41,3 +43,26 @@ func Avatar(ctx *context.Context) {

ctx.Redirect(user.RealSizedAvatarLink(size))
}

// AvatarByEmailHash redirects the browser to the appropriate Avatar link
func AvatarByEmailHash(ctx *context.Context) {
hash := ctx.Params(":hash")
if len(hash) == 0 {
ctx.ServerError("invalid avatar hash", errors.New("hash cannot be empty"))
return
}
email, err := models.GetEmailForHash(hash)
if err != nil {
ctx.ServerError("invalid avatar hash", err)
return
}
if len(email) == 0 {
ctx.Redirect(base.DefaultAvatarLink())
return
}
size := ctx.QueryInt("size")
if size == 0 {
size = base.DefaultAvatarSize
}
ctx.Redirect(base.SizedAvatarLinkWithDomain(email, size))
}