@@ -315,10 +315,9 @@ func (u *User) generateRandomAvatar(e Engine) error { | |||
return nil | |||
} | |||
// RelAvatarLink returns relative avatar link to the site domain, | |||
// which includes app sub-url as prefix. However, it is possible | |||
// to return full URL if user enables Gravatar-like service. | |||
func (u *User) RelAvatarLink() string { | |||
// SizedRelAvatarLink returns a relative link to the user's avatar. When | |||
// applicable, the link is for an avatar of the indicated size (in pixels). | |||
func (u *User) SizedRelAvatarLink(size int) string { | |||
if u.ID == -1 { | |||
return base.DefaultAvatarLink() | |||
} | |||
@@ -338,7 +337,14 @@ func (u *User) RelAvatarLink() string { | |||
return setting.AppSubURL + "/avatars/" + u.Avatar | |||
} | |||
return base.AvatarLink(u.AvatarEmail) | |||
return base.SizedAvatarLink(u.AvatarEmail, size) | |||
} | |||
// RelAvatarLink returns a relative link to the user's avatar. The link | |||
// may either be a sub-URL to this site, or a full URL to an external avatar | |||
// service. | |||
func (u *User) RelAvatarLink() string { | |||
return u.SizedRelAvatarLink(base.DefaultAvatarSize) | |||
} | |||
// AvatarLink returns user avatar absolute link. | |||
@@ -16,6 +16,8 @@ import ( | |||
"math" | |||
"math/big" | |||
"net/http" | |||
"net/url" | |||
"path" | |||
"strconv" | |||
"strings" | |||
"time" | |||
@@ -197,24 +199,59 @@ func DefaultAvatarLink() string { | |||
return setting.AppSubURL + "/img/avatar_default.png" | |||
} | |||
// 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 { | |||
// DefaultAvatarSize is a sentinel value for the default avatar size, as | |||
// 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(4, "LibravatarService.FromEmail(email=%s): error %v", email, err) | |||
return nil, err | |||
} | |||
u, err := url.Parse(urlStr) | |||
if err != nil { | |||
log.Error(4, "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 { | |||
url, err := setting.LibravatarService.FromEmail(email) | |||
var err error | |||
avatarURL, err = libravatarURL(email) | |||
if err != nil { | |||
log.Error(4, "LibravatarService.FromEmail(email=%s): error %v", email, err) | |||
return DefaultAvatarLink() | |||
} | |||
return url | |||
} else if !setting.DisableGravatar { | |||
// copy GravatarSourceURL, because we will modify its Path. | |||
copyOfGravatarSourceURL := *setting.GravatarSourceURL | |||
avatarURL = ©OfGravatarSourceURL | |||
avatarURL.Path = path.Join(avatarURL.Path, HashEmail(email)) | |||
} else { | |||
return DefaultAvatarLink() | |||
} | |||
if !setting.DisableGravatar { | |||
return setting.GravatarSource + HashEmail(email) + "?d=identicon" | |||
vals := avatarURL.Query() | |||
vals.Set("d", "identicon") | |||
if size != DefaultAvatarSize { | |||
vals.Set("s", strconv.Itoa(size)) | |||
} | |||
avatarURL.RawQuery = vals.Encode() | |||
return avatarURL.String() | |||
} | |||
return DefaultAvatarLink() | |||
// 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) | |||
} | |||
// Seconds-based time units | |||
@@ -1,11 +1,13 @@ | |||
package base | |||
import ( | |||
"net/url" | |||
"os" | |||
"testing" | |||
"time" | |||
"code.gitea.io/gitea/modules/setting" | |||
"github.com/Unknwon/i18n" | |||
macaroni18n "github.com/go-macaron/i18n" | |||
"github.com/stretchr/testify/assert" | |||
@@ -126,16 +128,40 @@ func TestHashEmail(t *testing.T) { | |||
) | |||
} | |||
func TestAvatarLink(t *testing.T) { | |||
const gravatarSource = "https://secure.gravatar.com/avatar/" | |||
func disableGravatar() { | |||
setting.EnableFederatedAvatar = false | |||
setting.LibravatarService = nil | |||
setting.DisableGravatar = true | |||
} | |||
assert.Equal(t, "/img/avatar_default.png", AvatarLink("")) | |||
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 TestAvatarLink(t *testing.T) { | |||
disableGravatar() | |||
assert.Equal(t, "/img/avatar_default.png", AvatarLink("gitea@example.com")) | |||
enableGravatar(t) | |||
assert.Equal(t, | |||
"353cbad9b58e69c96154ad99f92bedc7?d=identicon", | |||
"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon", | |||
AvatarLink("gitea@example.com"), | |||
) | |||
} | |||
@@ -326,6 +326,7 @@ var ( | |||
// Picture settings | |||
AvatarUploadPath string | |||
GravatarSource string | |||
GravatarSourceURL *url.URL | |||
DisableGravatar bool | |||
EnableFederatedAvatar bool | |||
LibravatarService *libravatar.Libravatar | |||
@@ -1027,18 +1028,22 @@ func NewContext() { | |||
if DisableGravatar { | |||
EnableFederatedAvatar = false | |||
} | |||
if EnableFederatedAvatar || !DisableGravatar { | |||
GravatarSourceURL, err = url.Parse(GravatarSource) | |||
if err != nil { | |||
log.Fatal(4, "Failed to parse Gravatar URL(%s): %v", | |||
GravatarSource, err) | |||
} | |||
} | |||
if EnableFederatedAvatar { | |||
LibravatarService = libravatar.New() | |||
parts := strings.Split(GravatarSource, "/") | |||
if len(parts) >= 3 { | |||
if parts[0] == "https:" { | |||
LibravatarService.SetUseHTTPS(true) | |||
LibravatarService.SetSecureFallbackHost(parts[2]) | |||
} else { | |||
LibravatarService.SetUseHTTPS(false) | |||
LibravatarService.SetFallbackHost(parts[2]) | |||
} | |||
if GravatarSourceURL.Scheme == "https" { | |||
LibravatarService.SetUseHTTPS(true) | |||
LibravatarService.SetSecureFallbackHost(GravatarSourceURL.Host) | |||
} else { | |||
LibravatarService.SetUseHTTPS(false) | |||
LibravatarService.SetFallbackHost(GravatarSourceURL.Host) | |||
} | |||
} | |||
@@ -3,7 +3,7 @@ | |||
<div class="ui vertically grid head"> | |||
<div class="column"> | |||
<div class="ui header"> | |||
<img class="ui image" src="{{.RelAvatarLink}}?s=100"> | |||
<img class="ui image" src="{{.SizedRelAvatarLink 100}}"> | |||
<span class="text thin grey"><a href="{{.HomeLink}}">{{.DisplayName}}</a></span> | |||
<div class="ui right"> | |||
@@ -3,7 +3,7 @@ | |||
<div class="ui container"> | |||
<div class="ui grid"> | |||
<div class="ui sixteen wide column"> | |||
<img class="ui left" id="org-avatar" src="{{.Org.RelAvatarLink}}?s=140"/> | |||
<img class="ui left" id="org-avatar" src="{{.Org.SizedRelAvatarLink 140}}"/> | |||
<div id="org-info"> | |||
<div class="ui header"> | |||
{{.Org.DisplayName}} | |||
@@ -8,7 +8,7 @@ | |||
{{range .Members}} | |||
<div class="item ui grid"> | |||
<div class="ui one wide column"> | |||
<img class="ui avatar" src="{{.RelAvatarLink}}?s=48"> | |||
<img class="ui avatar" src="{{.SizedRelAvatarLink 48}}"> | |||
</div> | |||
<div class="ui three wide column"> | |||
<div class="meta"><a href="{{.HomeLink}}">{{.Name}}</a></div> | |||
@@ -6,11 +6,11 @@ | |||
<div class="ui card"> | |||
{{if eq .SignedUserName .Owner.Name}} | |||
<a class="image poping up" href="{{AppSubUrl}}/user/settings/avatar" id="profile-avatar" data-content="{{.i18n.Tr "user.change_avatar"}}" data-variation="inverted tiny" data-position="bottom center"> | |||
<img src="{{.Owner.RelAvatarLink}}?s=290" title="{{.Owner.Name}}"/> | |||
<img src="{{.Owner.SizedRelAvatarLink 290}}" title="{{.Owner.Name}}"/> | |||
</a> | |||
{{else}} | |||
<span class="image"> | |||
<img src="{{.Owner.RelAvatarLink}}?s=290" title="{{.Owner.Name}}"/> | |||
<img src="{{.Owner.SizedRelAvatarLink 290}}" title="{{.Owner.Name}}"/> | |||
</span> | |||
{{end}} | |||
<div class="content"> | |||