summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorzeripath <art27@cantab.net>2020-03-27 12:34:39 +0000
committerGitHub <noreply@github.com>2020-03-27 14:34:39 +0200
commite6baa656f757fd1f2f6ba20c677e0c83422a8739 (patch)
treeb2a396f41e1b8a08b796084d169f202d593f7357
parenta3f90948d8fa4dd5c92e15cc10e86d2fec37f6e7 (diff)
downloadgitea-e6baa656f757fd1f2f6ba20c677e0c83422a8739.tar.gz
gitea-e6baa656f757fd1f2f6ba20c677e0c83422a8739.zip
make avatar lookup occur at image request (#10540)
speed up page generation by making avatar lookup occur at the browser not at page generation * Protect against evil email address ".." * hash the complete email address Signed-off-by: Andrew Thornton <art27@cantab.net> Co-Authored-By: Lauris BH <lauris@nix.lv>
-rw-r--r--models/avatar.go48
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v133.go16
-rw-r--r--models/models.go1
-rw-r--r--modules/base/tool.go31
-rw-r--r--modules/base/tool_test.go11
-rw-r--r--modules/cache/cache.go28
-rw-r--r--modules/repository/commits.go3
-rw-r--r--modules/repository/commits_test.go4
-rw-r--r--modules/templates/helper.go2
-rw-r--r--routers/repo/blame.go2
-rw-r--r--routers/routes/routes.go2
-rw-r--r--routers/user/avatar.go25
13 files changed, 154 insertions, 21 deletions
diff --git a/models/avatar.go b/models/avatar.go
new file mode 100644
index 0000000000..311d714629
--- /dev/null
+++ b/models/avatar.go
@@ -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 varchar(32)"`
+ 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)
+}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index c554121e85..3f18a18c6d 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -198,6 +198,8 @@ var migrations = []Migration{
NewMigration("Add IsSystemWebhook column to webhooks table", addSystemWebhookColumn),
// v132 -> v133
NewMigration("Add Branch Protection Protected Files Column", addBranchProtectionProtectedFilesColumn),
+ // v133 -> v134
+ NewMigration("Add EmailHash Table", addEmailHashTable),
}
// Migrate database to current version
diff --git a/models/migrations/v133.go b/models/migrations/v133.go
new file mode 100644
index 0000000000..ea0411d470
--- /dev/null
+++ b/models/migrations/v133.go
@@ -0,0 +1,16 @@
+// 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 migrations
+
+import "xorm.io/xorm"
+
+func addEmailHashTable(x *xorm.Engine) error {
+ // EmailHash represents a pre-generated hash map
+ type EmailHash struct {
+ Hash string `xorm:"pk varchar(32)"`
+ Email string `xorm:"UNIQUE NOT NULL"`
+ }
+ return x.Sync2(new(EmailHash))
+}
diff --git a/models/models.go b/models/models.go
index d2872422e3..3bf7713955 100644
--- a/models/models.go
+++ b/models/models.go
@@ -124,6 +124,7 @@ func init() {
new(OAuth2Grant),
new(Task),
new(LanguageStat),
+ new(EmailHash),
)
gonicNames := []string{"SSL", "UID"}
diff --git a/modules/base/tool.go b/modules/base/tool.go
index 86606c8bee..157bd9bc3d 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -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.
diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go
index 075b5ed817..9c1a79e3f2 100644
--- a/modules/base/tool_test.go
+++ b/modules/base/tool_test.go
@@ -90,17 +90,6 @@ func TestSizedAvatarLink(t *testing.T) {
)
}
-func TestAvatarLink(t *testing.T) {
- disableGravatar()
- assert.Equal(t, "/img/avatar_default.png", AvatarLink("gitea@example.com"))
-
- enableGravatar(t)
- assert.Equal(t,
- "https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon",
- AvatarLink("gitea@example.com"),
- )
-}
-
func TestFileSize(t *testing.T) {
var size int64 = 512
assert.Equal(t, "512 B", FileSize(size))
diff --git a/modules/cache/cache.go b/modules/cache/cache.go
index e3a905e3fa..859f4a4b47 100644
--- a/modules/cache/cache.go
+++ b/modules/cache/cache.go
@@ -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 {
diff --git a/modules/repository/commits.go b/modules/repository/commits.go
index 7345aaae24..e02f3d11ca 100644
--- a/modules/repository/commits.go
+++ b/modules/repository/commits.go
@@ -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"
@@ -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 ""
diff --git a/modules/repository/commits_test.go b/modules/repository/commits_test.go
index 2f61ce3329..cb00e19c2e 100644
--- a/modules/repository/commits_test.go
+++ b/modules/repository/commits_test.go
@@ -6,6 +6,8 @@ package repository
import (
"container/list"
+ "crypto/md5"
+ "fmt"
"testing"
"time"
@@ -114,7 +116,7 @@ func TestPushCommits_AvatarLink(t *testing.T) {
pushCommits.AvatarLink("user2@example.com"))
assert.Equal(t,
- "https://secure.gravatar.com/avatar/19ade630b94e1e0535b3df7387434154?d=identicon",
+ "/avatar/"+fmt.Sprintf("%x", md5.Sum([]byte("nonexistent@example.com"))),
pushCommits.AvatarLink("nonexistent@example.com"))
}
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 9d3206934e..b5b4987427 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -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,
diff --git a/routers/repo/blame.go b/routers/repo/blame.go
index f5a2a548e3..beed59ea97 100644
--- a/routers/repo/blame.go
+++ b/routers/repo/blame.go
@@ -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 {
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index 093edcd920..459aa4d09f 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -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 *****
diff --git a/routers/user/avatar.go b/routers/user/avatar.go
index 045206c50a..32d05f03cc 100644
--- a/routers/user/avatar.go
+++ b/routers/user/avatar.go
@@ -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"
)
@@ -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))
+}