Skip to content

Commit 9269a03

Browse files
authored
Direct avatar rendering (#13649)
* Direct avatar rendering This adds new template helpers for avatar rendering which output image elements with direct links to avatars which makes them cacheable by the browsers. This should be a major performance improvment for pages with many avatars. * fix avatars of other user's profile pages * fix top border on user avatar name * uncircle avatars * remove old incomplete avatar selector * use title attribute for name and add it back on blame * minor refactor * tweak comments * fix url path join and adjust test to new result * dedupe functions
1 parent 0d35ef5 commit 9269a03

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+435
-340
lines changed

models/action.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,6 @@ func (a *Action) GetDisplayNameTitle() string {
140140
return a.GetActFullName()
141141
}
142142

143-
// GetActAvatar the action's user's avatar link
144-
func (a *Action) GetActAvatar() string {
145-
a.loadActUser()
146-
return a.ActUser.RelAvatarLink()
147-
}
148-
149143
// GetRepoUserName returns the name of the action repository owner.
150144
func (a *Action) GetRepoUserName() string {
151145
a.loadRepo()

models/avatar.go

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ import (
88
"crypto/md5"
99
"fmt"
1010
"net/url"
11+
"path"
12+
"strconv"
1113
"strings"
1214

15+
"code.gitea.io/gitea/modules/base"
1316
"code.gitea.io/gitea/modules/cache"
17+
"code.gitea.io/gitea/modules/log"
1418
"code.gitea.io/gitea/modules/setting"
1519
)
1620

@@ -20,6 +24,28 @@ type EmailHash struct {
2024
Email string `xorm:"UNIQUE NOT NULL"`
2125
}
2226

27+
// DefaultAvatarLink the default avatar link
28+
func DefaultAvatarLink() string {
29+
u, err := url.Parse(setting.AppSubURL)
30+
if err != nil {
31+
log.Error("GetUserByEmail: %v", err)
32+
return ""
33+
}
34+
35+
u.Path = path.Join(u.Path, "/img/avatar_default.png")
36+
return u.String()
37+
}
38+
39+
// DefaultAvatarSize is a sentinel value for the default avatar size, as
40+
// determined by the avatar-hosting service.
41+
const DefaultAvatarSize = -1
42+
43+
// HashEmail hashes email address to MD5 string.
44+
// https://en.gravatar.com/site/implement/hash/
45+
func HashEmail(email string) string {
46+
return base.EncodeMD5(strings.ToLower(strings.TrimSpace(email)))
47+
}
48+
2349
// GetEmailForHash converts a provided md5sum to the email
2450
func GetEmailForHash(md5Sum string) (string, error) {
2551
return cache.GetString("Avatar:"+md5Sum, func() (string, error) {
@@ -32,8 +58,24 @@ func GetEmailForHash(md5Sum string) (string, error) {
3258
})
3359
}
3460

35-
// AvatarLink returns an avatar link for a provided email
36-
func AvatarLink(email string) string {
61+
// LibravatarURL returns the URL for the given email. This function should only
62+
// be called if a federated avatar service is enabled.
63+
func LibravatarURL(email string) (*url.URL, error) {
64+
urlStr, err := setting.LibravatarService.FromEmail(email)
65+
if err != nil {
66+
log.Error("LibravatarService.FromEmail(email=%s): error %v", email, err)
67+
return nil, err
68+
}
69+
u, err := url.Parse(urlStr)
70+
if err != nil {
71+
log.Error("Failed to parse libravatar url(%s): error %v", urlStr, err)
72+
return nil, err
73+
}
74+
return u, nil
75+
}
76+
77+
// HashedAvatarLink returns an avatar link for a provided email
78+
func HashedAvatarLink(email string) string {
3779
lowerEmail := strings.ToLower(strings.TrimSpace(email))
3880
sum := fmt.Sprintf("%x", md5.Sum([]byte(lowerEmail)))
3981
_, _ = cache.GetString("Avatar:"+sum, func() (string, error) {
@@ -57,3 +99,34 @@ func AvatarLink(email string) string {
5799
})
58100
return setting.AppSubURL + "/avatar/" + url.PathEscape(sum)
59101
}
102+
103+
// MakeFinalAvatarURL constructs the final avatar URL string
104+
func MakeFinalAvatarURL(u *url.URL, size int) string {
105+
vals := u.Query()
106+
vals.Set("d", "identicon")
107+
if size != DefaultAvatarSize {
108+
vals.Set("s", strconv.Itoa(size))
109+
}
110+
u.RawQuery = vals.Encode()
111+
return u.String()
112+
}
113+
114+
// SizedAvatarLink returns a sized link to the avatar for the given email address.
115+
func SizedAvatarLink(email string, size int) string {
116+
var avatarURL *url.URL
117+
if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
118+
// This is the slow path that would need to call LibravatarURL() which
119+
// does DNS lookups. Avoid it by issuing a redirect so we don't block
120+
// the template render with network requests.
121+
return HashedAvatarLink(email)
122+
} else if !setting.DisableGravatar {
123+
// copy GravatarSourceURL, because we will modify its Path.
124+
copyOfGravatarSourceURL := *setting.GravatarSourceURL
125+
avatarURL = &copyOfGravatarSourceURL
126+
avatarURL.Path = path.Join(avatarURL.Path, HashEmail(email))
127+
} else {
128+
return DefaultAvatarLink()
129+
}
130+
131+
return MakeFinalAvatarURL(avatarURL, size)
132+
}

models/avatar_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2020 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package models
6+
7+
import (
8+
"net/url"
9+
"testing"
10+
11+
"code.gitea.io/gitea/modules/setting"
12+
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
const gravatarSource = "https://secure.gravatar.com/avatar/"
17+
18+
func disableGravatar() {
19+
setting.EnableFederatedAvatar = false
20+
setting.LibravatarService = nil
21+
setting.DisableGravatar = true
22+
}
23+
24+
func enableGravatar(t *testing.T) {
25+
setting.DisableGravatar = false
26+
var err error
27+
setting.GravatarSourceURL, err = url.Parse(gravatarSource)
28+
assert.NoError(t, err)
29+
}
30+
31+
func TestHashEmail(t *testing.T) {
32+
assert.Equal(t,
33+
"d41d8cd98f00b204e9800998ecf8427e",
34+
HashEmail(""),
35+
)
36+
assert.Equal(t,
37+
"353cbad9b58e69c96154ad99f92bedc7",
38+
HashEmail("[email protected]"),
39+
)
40+
}
41+
42+
func TestSizedAvatarLink(t *testing.T) {
43+
disableGravatar()
44+
assert.Equal(t, "/suburl/img/avatar_default.png",
45+
SizedAvatarLink("[email protected]", 100))
46+
47+
enableGravatar(t)
48+
assert.Equal(t,
49+
"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100",
50+
SizedAvatarLink("[email protected]", 100),
51+
)
52+
}

models/user_avatar.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"strings"
1414

1515
"code.gitea.io/gitea/modules/avatar"
16-
"code.gitea.io/gitea/modules/base"
1716
"code.gitea.io/gitea/modules/log"
1817
"code.gitea.io/gitea/modules/setting"
1918
"code.gitea.io/gitea/modules/storage"
@@ -41,7 +40,7 @@ func (u *User) generateRandomAvatar(e Engine) error {
4140
}
4241

4342
if u.Avatar == "" {
44-
u.Avatar = base.HashEmail(u.AvatarEmail)
43+
u.Avatar = HashEmail(u.AvatarEmail)
4544
}
4645

4746
if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
@@ -76,13 +75,13 @@ func (u *User) SizedRelAvatarLink(size int) string {
7675
//
7776
func (u *User) RealSizedAvatarLink(size int) string {
7877
if u.ID == -1 {
79-
return base.DefaultAvatarLink()
78+
return DefaultAvatarLink()
8079
}
8180

8281
switch {
8382
case u.UseCustomAvatar:
8483
if u.Avatar == "" {
85-
return base.DefaultAvatarLink()
84+
return DefaultAvatarLink()
8685
}
8786
return setting.AppSubURL + "/avatars/" + u.Avatar
8887
case setting.DisableGravatar, setting.OfflineMode:
@@ -94,14 +93,14 @@ func (u *User) RealSizedAvatarLink(size int) string {
9493

9594
return setting.AppSubURL + "/avatars/" + u.Avatar
9695
}
97-
return base.SizedAvatarLink(u.AvatarEmail, size)
96+
return SizedAvatarLink(u.AvatarEmail, size)
9897
}
9998

10099
// RelAvatarLink returns a relative link to the user's avatar. The link
101100
// may either be a sub-URL to this site, or a full URL to an external avatar
102101
// service.
103102
func (u *User) RelAvatarLink() string {
104-
return u.SizedRelAvatarLink(base.DefaultAvatarSize)
103+
return u.SizedRelAvatarLink(DefaultAvatarSize)
105104
}
106105

107106
// AvatarLink returns user avatar absolute link.

modules/auth/sso/sspi_windows.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ func (s *SSPI) newUser(ctx *macaron.Context, username string, cfg *models.SSPICo
168168
IsActive: cfg.AutoActivateUsers,
169169
Language: cfg.DefaultLanguage,
170170
UseCustomAvatar: true,
171-
Avatar: base.DefaultAvatarLink(),
171+
Avatar: models.DefaultAvatarLink(),
172172
EmailNotificationsPreference: models.EmailNotificationsDisabled,
173173
}
174174
if err := models.CreateUser(user); err != nil {

modules/base/tool.go

Lines changed: 0 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ import (
1212
"encoding/hex"
1313
"fmt"
1414
"net/http"
15-
"net/url"
1615
"os"
17-
"path"
1816
"path/filepath"
1917
"runtime"
2018
"strconv"
@@ -134,93 +132,6 @@ func CreateTimeLimitCode(data string, minutes int, startInf interface{}) string
134132
return code
135133
}
136134

137-
// HashEmail hashes email address to MD5 string.
138-
// https://en.gravatar.com/site/implement/hash/
139-
func HashEmail(email string) string {
140-
return EncodeMD5(strings.ToLower(strings.TrimSpace(email)))
141-
}
142-
143-
// DefaultAvatarLink the default avatar link
144-
func DefaultAvatarLink() string {
145-
return setting.AppSubURL + "/img/avatar_default.png"
146-
}
147-
148-
// DefaultAvatarSize is a sentinel value for the default avatar size, as
149-
// determined by the avatar-hosting service.
150-
const DefaultAvatarSize = -1
151-
152-
// libravatarURL returns the URL for the given email. This function should only
153-
// be called if a federated avatar service is enabled.
154-
func libravatarURL(email string) (*url.URL, error) {
155-
urlStr, err := setting.LibravatarService.FromEmail(email)
156-
if err != nil {
157-
log.Error("LibravatarService.FromEmail(email=%s): error %v", email, err)
158-
return nil, err
159-
}
160-
u, err := url.Parse(urlStr)
161-
if err != nil {
162-
log.Error("Failed to parse libravatar url(%s): error %v", urlStr, err)
163-
return nil, err
164-
}
165-
return u, nil
166-
}
167-
168-
// SizedAvatarLink returns a sized link to the avatar for the given email
169-
// address.
170-
func SizedAvatarLink(email string, size int) string {
171-
var avatarURL *url.URL
172-
if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
173-
var err error
174-
avatarURL, err = libravatarURL(email)
175-
if err != nil {
176-
return DefaultAvatarLink()
177-
}
178-
} else if !setting.DisableGravatar {
179-
// copy GravatarSourceURL, because we will modify its Path.
180-
copyOfGravatarSourceURL := *setting.GravatarSourceURL
181-
avatarURL = &copyOfGravatarSourceURL
182-
avatarURL.Path = path.Join(avatarURL.Path, HashEmail(email))
183-
} else {
184-
return DefaultAvatarLink()
185-
}
186-
187-
vals := avatarURL.Query()
188-
vals.Set("d", "identicon")
189-
if size != DefaultAvatarSize {
190-
vals.Set("s", strconv.Itoa(size))
191-
}
192-
avatarURL.RawQuery = vals.Encode()
193-
return avatarURL.String()
194-
}
195-
196-
// SizedAvatarLinkWithDomain returns a sized link to the avatar for the given email
197-
// address.
198-
func SizedAvatarLinkWithDomain(email string, size int) string {
199-
var avatarURL *url.URL
200-
if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
201-
var err error
202-
avatarURL, err = libravatarURL(email)
203-
if err != nil {
204-
return DefaultAvatarLink()
205-
}
206-
} else if !setting.DisableGravatar {
207-
// copy GravatarSourceURL, because we will modify its Path.
208-
copyOfGravatarSourceURL := *setting.GravatarSourceURL
209-
avatarURL = &copyOfGravatarSourceURL
210-
avatarURL.Path = path.Join(avatarURL.Path, HashEmail(email))
211-
} else {
212-
return DefaultAvatarLink()
213-
}
214-
215-
vals := avatarURL.Query()
216-
vals.Set("d", "identicon")
217-
if size != DefaultAvatarSize {
218-
vals.Set("s", strconv.Itoa(size))
219-
}
220-
avatarURL.RawQuery = vals.Encode()
221-
return avatarURL.String()
222-
}
223-
224135
// FileSize calculates the file size and generate user-friendly string.
225136
func FileSize(s int64) string {
226137
return humanize.IBytes(uint64(s))

modules/base/tool_test.go

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@
55
package base
66

77
import (
8-
"net/url"
98
"testing"
109

11-
"code.gitea.io/gitea/modules/setting"
12-
1310
"github.com/stretchr/testify/assert"
1411
)
1512

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

59-
func TestHashEmail(t *testing.T) {
60-
assert.Equal(t,
61-
"d41d8cd98f00b204e9800998ecf8427e",
62-
HashEmail(""),
63-
)
64-
assert.Equal(t,
65-
"353cbad9b58e69c96154ad99f92bedc7",
66-
HashEmail("[email protected]"),
67-
)
68-
}
69-
70-
const gravatarSource = "https://secure.gravatar.com/avatar/"
71-
72-
func disableGravatar() {
73-
setting.EnableFederatedAvatar = false
74-
setting.LibravatarService = nil
75-
setting.DisableGravatar = true
76-
}
77-
78-
func enableGravatar(t *testing.T) {
79-
setting.DisableGravatar = false
80-
var err error
81-
setting.GravatarSourceURL, err = url.Parse(gravatarSource)
82-
assert.NoError(t, err)
83-
}
84-
85-
func TestSizedAvatarLink(t *testing.T) {
86-
disableGravatar()
87-
assert.Equal(t, "/img/avatar_default.png",
88-
SizedAvatarLink("[email protected]", 100))
89-
90-
enableGravatar(t)
91-
assert.Equal(t,
92-
"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100",
93-
SizedAvatarLink("[email protected]", 100),
94-
)
95-
}
96-
9756
func TestFileSize(t *testing.T) {
9857
var size int64 = 512
9958
assert.Equal(t, "512 B", FileSize(size))

modules/repository/commits.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func (pc *PushCommits) AvatarLink(email string) string {
123123
var err error
124124
u, err = models.GetUserByEmail(email)
125125
if err != nil {
126-
pc.avatars[email] = models.AvatarLink(email)
126+
pc.avatars[email] = models.HashedAvatarLink(email)
127127
if !models.IsErrUserNotExist(err) {
128128
log.Error("GetUserByEmail: %v", err)
129129
return ""

0 commit comments

Comments
 (0)