| @@ -315,10 +315,9 @@ func (u *User) generateRandomAvatar(e Engine) error { | |||||
| return nil | 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 { | if u.ID == -1 { | ||||
| return base.DefaultAvatarLink() | return base.DefaultAvatarLink() | ||||
| } | } | ||||
| @@ -338,7 +337,14 @@ func (u *User) RelAvatarLink() string { | |||||
| return setting.AppSubURL + "/avatars/" + u.Avatar | 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. | // AvatarLink returns user avatar absolute link. | ||||
| @@ -16,6 +16,8 @@ import ( | |||||
| "math" | "math" | ||||
| "math/big" | "math/big" | ||||
| "net/http" | "net/http" | ||||
| "net/url" | |||||
| "path" | |||||
| "strconv" | "strconv" | ||||
| "strings" | "strings" | ||||
| "time" | "time" | ||||
| @@ -197,24 +199,59 @@ func DefaultAvatarLink() string { | |||||
| return setting.AppSubURL + "/img/avatar_default.png" | 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 { | if setting.EnableFederatedAvatar && setting.LibravatarService != nil { | ||||
| url, err := setting.LibravatarService.FromEmail(email) | |||||
| var err error | |||||
| avatarURL, err = libravatarURL(email) | |||||
| if err != nil { | if err != nil { | ||||
| log.Error(4, "LibravatarService.FromEmail(email=%s): error %v", email, err) | |||||
| return DefaultAvatarLink() | 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 | // Seconds-based time units | ||||
| @@ -1,11 +1,13 @@ | |||||
| package base | package base | ||||
| import ( | import ( | ||||
| "net/url" | |||||
| "os" | "os" | ||||
| "testing" | "testing" | ||||
| "time" | "time" | ||||
| "code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
| "github.com/Unknwon/i18n" | "github.com/Unknwon/i18n" | ||||
| macaroni18n "github.com/go-macaron/i18n" | macaroni18n "github.com/go-macaron/i18n" | ||||
| "github.com/stretchr/testify/assert" | "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.EnableFederatedAvatar = false | ||||
| setting.LibravatarService = nil | setting.LibravatarService = nil | ||||
| setting.DisableGravatar = true | setting.DisableGravatar = true | ||||
| } | |||||
| assert.Equal(t, "/img/avatar_default.png", AvatarLink("")) | |||||
| func enableGravatar(t *testing.T) { | |||||
| setting.DisableGravatar = false | 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, | assert.Equal(t, | ||||
| "353cbad9b58e69c96154ad99f92bedc7?d=identicon", | |||||
| "https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon", | |||||
| AvatarLink("gitea@example.com"), | AvatarLink("gitea@example.com"), | ||||
| ) | ) | ||||
| } | } | ||||
| @@ -326,6 +326,7 @@ var ( | |||||
| // Picture settings | // Picture settings | ||||
| AvatarUploadPath string | AvatarUploadPath string | ||||
| GravatarSource string | GravatarSource string | ||||
| GravatarSourceURL *url.URL | |||||
| DisableGravatar bool | DisableGravatar bool | ||||
| EnableFederatedAvatar bool | EnableFederatedAvatar bool | ||||
| LibravatarService *libravatar.Libravatar | LibravatarService *libravatar.Libravatar | ||||
| @@ -1027,18 +1028,22 @@ func NewContext() { | |||||
| if DisableGravatar { | if DisableGravatar { | ||||
| EnableFederatedAvatar = false | 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 { | if EnableFederatedAvatar { | ||||
| LibravatarService = libravatar.New() | 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="ui vertically grid head"> | ||||
| <div class="column"> | <div class="column"> | ||||
| <div class="ui header"> | <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> | <span class="text thin grey"><a href="{{.HomeLink}}">{{.DisplayName}}</a></span> | ||||
| <div class="ui right"> | <div class="ui right"> | ||||
| @@ -3,7 +3,7 @@ | |||||
| <div class="ui container"> | <div class="ui container"> | ||||
| <div class="ui grid"> | <div class="ui grid"> | ||||
| <div class="ui sixteen wide column"> | <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 id="org-info"> | ||||
| <div class="ui header"> | <div class="ui header"> | ||||
| {{.Org.DisplayName}} | {{.Org.DisplayName}} | ||||
| @@ -8,7 +8,7 @@ | |||||
| {{range .Members}} | {{range .Members}} | ||||
| <div class="item ui grid"> | <div class="item ui grid"> | ||||
| <div class="ui one wide column"> | <div class="ui one wide column"> | ||||
| <img class="ui avatar" src="{{.RelAvatarLink}}?s=48"> | |||||
| <img class="ui avatar" src="{{.SizedRelAvatarLink 48}}"> | |||||
| </div> | </div> | ||||
| <div class="ui three wide column"> | <div class="ui three wide column"> | ||||
| <div class="meta"><a href="{{.HomeLink}}">{{.Name}}</a></div> | <div class="meta"><a href="{{.HomeLink}}">{{.Name}}</a></div> | ||||
| @@ -6,11 +6,11 @@ | |||||
| <div class="ui card"> | <div class="ui card"> | ||||
| {{if eq .SignedUserName .Owner.Name}} | {{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"> | <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> | </a> | ||||
| {{else}} | {{else}} | ||||
| <span class="image"> | <span class="image"> | ||||
| <img src="{{.Owner.RelAvatarLink}}?s=290" title="{{.Owner.Name}}"/> | |||||
| <img src="{{.Owner.SizedRelAvatarLink 290}}" title="{{.Owner.Name}}"/> | |||||
| </span> | </span> | ||||
| {{end}} | {{end}} | ||||
| <div class="content"> | <div class="content"> | ||||