Browse Source

Fix avatar URLs (#3069)

* Fix avatar URLs

* import order
Ethan Koenig 1 year ago
parent
commit
ab62da283a

+ 11 - 5
models/user.go

@@ -315,10 +315,9 @@ func (u *User) generateRandomAvatar(e Engine) error {
315 315
 	return nil
316 316
 }
317 317
 
318
-// RelAvatarLink returns relative avatar link to the site domain,
319
-// which includes app sub-url as prefix. However, it is possible
320
-// to return full URL if user enables Gravatar-like service.
321
-func (u *User) RelAvatarLink() string {
318
+// SizedRelAvatarLink returns a relative link to the user's avatar. When
319
+// applicable, the link is for an avatar of the indicated size (in pixels).
320
+func (u *User) SizedRelAvatarLink(size int) string {
322 321
 	if u.ID == -1 {
323 322
 		return base.DefaultAvatarLink()
324 323
 	}
@@ -338,7 +337,14 @@ func (u *User) RelAvatarLink() string {
338 337
 
339 338
 		return setting.AppSubURL + "/avatars/" + u.Avatar
340 339
 	}
341
-	return base.AvatarLink(u.AvatarEmail)
340
+	return base.SizedAvatarLink(u.AvatarEmail, size)
341
+}
342
+
343
+// RelAvatarLink returns a relative link to the user's avatar. The link
344
+// may either be a sub-URL to this site, or a full URL to an external avatar
345
+// service.
346
+func (u *User) RelAvatarLink() string {
347
+	return u.SizedRelAvatarLink(base.DefaultAvatarSize)
342 348
 }
343 349
 
344 350
 // AvatarLink returns user avatar absolute link.

+ 47 - 10
modules/base/tool.go

@@ -16,6 +16,8 @@ import (
16 16
 	"math"
17 17
 	"math/big"
18 18
 	"net/http"
19
+	"net/url"
20
+	"path"
19 21
 	"strconv"
20 22
 	"strings"
21 23
 	"time"
@@ -197,24 +199,59 @@ func DefaultAvatarLink() string {
197 199
 	return setting.AppSubURL + "/img/avatar_default.png"
198 200
 }
199 201
 
200
-// AvatarLink returns relative avatar link to the site domain by given email,
201
-// which includes app sub-url as prefix. However, it is possible
202
-// to return full URL if user enables Gravatar-like service.
203
-func AvatarLink(email string) string {
202
+// DefaultAvatarSize is a sentinel value for the default avatar size, as
203
+// determined by the avatar-hosting service.
204
+const DefaultAvatarSize = -1
205
+
206
+// libravatarURL returns the URL for the given email. This function should only
207
+// be called if a federated avatar service is enabled.
208
+func libravatarURL(email string) (*url.URL, error) {
209
+	urlStr, err := setting.LibravatarService.FromEmail(email)
210
+	if err != nil {
211
+		log.Error(4, "LibravatarService.FromEmail(email=%s): error %v", email, err)
212
+		return nil, err
213
+	}
214
+	u, err := url.Parse(urlStr)
215
+	if err != nil {
216
+		log.Error(4, "Failed to parse libravatar url(%s): error %v", urlStr, err)
217
+		return nil, err
218
+	}
219
+	return u, nil
220
+}
221
+
222
+// SizedAvatarLink returns a sized link to the avatar for the given email
223
+// address.
224
+func SizedAvatarLink(email string, size int) string {
225
+	var avatarURL *url.URL
204 226
 	if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
205
-		url, err := setting.LibravatarService.FromEmail(email)
227
+		var err error
228
+		avatarURL, err = libravatarURL(email)
206 229
 		if err != nil {
207
-			log.Error(4, "LibravatarService.FromEmail(email=%s): error %v", email, err)
208 230
 			return DefaultAvatarLink()
209 231
 		}
210
-		return url
232
+	} else if !setting.DisableGravatar {
233
+		// copy GravatarSourceURL, because we will modify its Path.
234
+		copyOfGravatarSourceURL := *setting.GravatarSourceURL
235
+		avatarURL = &copyOfGravatarSourceURL
236
+		avatarURL.Path = path.Join(avatarURL.Path, HashEmail(email))
237
+	} else {
238
+		return DefaultAvatarLink()
211 239
 	}
212 240
 
213
-	if !setting.DisableGravatar {
214
-		return setting.GravatarSource + HashEmail(email) + "?d=identicon"
241
+	vals := avatarURL.Query()
242
+	vals.Set("d", "identicon")
243
+	if size != DefaultAvatarSize {
244
+		vals.Set("s", strconv.Itoa(size))
215 245
 	}
246
+	avatarURL.RawQuery = vals.Encode()
247
+	return avatarURL.String()
248
+}
216 249
 
217
-	return DefaultAvatarLink()
250
+// AvatarLink returns relative avatar link to the site domain by given email,
251
+// which includes app sub-url as prefix. However, it is possible
252
+// to return full URL if user enables Gravatar-like service.
253
+func AvatarLink(email string) string {
254
+	return SizedAvatarLink(email, DefaultAvatarSize)
218 255
 }
219 256
 
220 257
 // Seconds-based time units

+ 30 - 4
modules/base/tool_test.go

@@ -1,11 +1,13 @@
1 1
 package base
2 2
 
3 3
 import (
4
+	"net/url"
4 5
 	"os"
5 6
 	"testing"
6 7
 	"time"
7 8
 
8 9
 	"code.gitea.io/gitea/modules/setting"
10
+
9 11
 	"github.com/Unknwon/i18n"
10 12
 	macaroni18n "github.com/go-macaron/i18n"
11 13
 	"github.com/stretchr/testify/assert"
@@ -126,16 +128,40 @@ func TestHashEmail(t *testing.T) {
126 128
 	)
127 129
 }
128 130
 
129
-func TestAvatarLink(t *testing.T) {
131
+const gravatarSource = "https://secure.gravatar.com/avatar/"
132
+
133
+func disableGravatar() {
130 134
 	setting.EnableFederatedAvatar = false
131 135
 	setting.LibravatarService = nil
132 136
 	setting.DisableGravatar = true
137
+}
133 138
 
134
-	assert.Equal(t, "/img/avatar_default.png", AvatarLink(""))
135
-
139
+func enableGravatar(t *testing.T) {
136 140
 	setting.DisableGravatar = false
141
+	var err error
142
+	setting.GravatarSourceURL, err = url.Parse(gravatarSource)
143
+	assert.NoError(t, err)
144
+}
145
+
146
+func TestSizedAvatarLink(t *testing.T) {
147
+	disableGravatar()
148
+	assert.Equal(t, "/img/avatar_default.png",
149
+		SizedAvatarLink("gitea@example.com", 100))
150
+
151
+	enableGravatar(t)
152
+	assert.Equal(t,
153
+		"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100",
154
+		SizedAvatarLink("gitea@example.com", 100),
155
+	)
156
+}
157
+
158
+func TestAvatarLink(t *testing.T) {
159
+	disableGravatar()
160
+	assert.Equal(t, "/img/avatar_default.png", AvatarLink("gitea@example.com"))
161
+
162
+	enableGravatar(t)
137 163
 	assert.Equal(t,
138
-		"353cbad9b58e69c96154ad99f92bedc7?d=identicon",
164
+		"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon",
139 165
 		AvatarLink("gitea@example.com"),
140 166
 	)
141 167
 }

+ 14 - 9
modules/setting/setting.go

@@ -326,6 +326,7 @@ var (
326 326
 	// Picture settings
327 327
 	AvatarUploadPath      string
328 328
 	GravatarSource        string
329
+	GravatarSourceURL     *url.URL
329 330
 	DisableGravatar       bool
330 331
 	EnableFederatedAvatar bool
331 332
 	LibravatarService     *libravatar.Libravatar
@@ -1027,18 +1028,22 @@ func NewContext() {
1027 1028
 	if DisableGravatar {
1028 1029
 		EnableFederatedAvatar = false
1029 1030
 	}
1031
+	if EnableFederatedAvatar || !DisableGravatar {
1032
+		GravatarSourceURL, err = url.Parse(GravatarSource)
1033
+		if err != nil {
1034
+			log.Fatal(4, "Failed to parse Gravatar URL(%s): %v",
1035
+				GravatarSource, err)
1036
+		}
1037
+	}
1030 1038
 
1031 1039
 	if EnableFederatedAvatar {
1032 1040
 		LibravatarService = libravatar.New()
1033
-		parts := strings.Split(GravatarSource, "/")
1034
-		if len(parts) >= 3 {
1035
-			if parts[0] == "https:" {
1036
-				LibravatarService.SetUseHTTPS(true)
1037
-				LibravatarService.SetSecureFallbackHost(parts[2])
1038
-			} else {
1039
-				LibravatarService.SetUseHTTPS(false)
1040
-				LibravatarService.SetFallbackHost(parts[2])
1041
-			}
1041
+		if GravatarSourceURL.Scheme == "https" {
1042
+			LibravatarService.SetUseHTTPS(true)
1043
+			LibravatarService.SetSecureFallbackHost(GravatarSourceURL.Host)
1044
+		} else {
1045
+			LibravatarService.SetUseHTTPS(false)
1046
+			LibravatarService.SetFallbackHost(GravatarSourceURL.Host)
1042 1047
 		}
1043 1048
 	}
1044 1049
 

+ 1 - 1
templates/org/header.tmpl

@@ -3,7 +3,7 @@
3 3
 		<div class="ui vertically grid head">
4 4
 			<div class="column">
5 5
 				<div class="ui header">
6
-					<img class="ui image" src="{{.RelAvatarLink}}?s=100">
6
+					<img class="ui image" src="{{.SizedRelAvatarLink 100}}">
7 7
 					<span class="text thin grey"><a href="{{.HomeLink}}">{{.DisplayName}}</a></span>
8 8
 
9 9
 					<div class="ui right">

+ 1 - 1
templates/org/home.tmpl

@@ -3,7 +3,7 @@
3 3
 	<div class="ui container">
4 4
 		<div class="ui grid">
5 5
 			<div class="ui sixteen wide column">
6
-				<img class="ui left" id="org-avatar" src="{{.Org.RelAvatarLink}}?s=140"/>
6
+				<img class="ui left" id="org-avatar" src="{{.Org.SizedRelAvatarLink 140}}"/>
7 7
 				<div id="org-info">
8 8
 					<div class="ui header">
9 9
 						{{.Org.DisplayName}}

+ 1 - 1
templates/org/member/members.tmpl

@@ -8,7 +8,7 @@
8 8
 			{{range .Members}}
9 9
 				<div class="item ui grid">
10 10
 					<div class="ui one wide column">
11
-						<img class="ui avatar" src="{{.RelAvatarLink}}?s=48">
11
+						<img class="ui avatar" src="{{.SizedRelAvatarLink 48}}">
12 12
 					</div>
13 13
 					<div class="ui three wide column">
14 14
 						<div class="meta"><a href="{{.HomeLink}}">{{.Name}}</a></div>

+ 2 - 2
templates/user/profile.tmpl

@@ -6,11 +6,11 @@
6 6
 				<div class="ui card">
7 7
 					{{if eq .SignedUserName .Owner.Name}}
8 8
 						<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">
9
-							<img src="{{.Owner.RelAvatarLink}}?s=290" title="{{.Owner.Name}}"/>
9
+							<img src="{{.Owner.SizedRelAvatarLink 290}}" title="{{.Owner.Name}}"/>
10 10
 						</a>
11 11
 					{{else}}
12 12
 						<span class="image">
13
-							<img src="{{.Owner.RelAvatarLink}}?s=290" title="{{.Owner.Name}}"/>
13
+							<img src="{{.Owner.SizedRelAvatarLink 290}}" title="{{.Owner.Name}}"/>
14 14
 						</span>
15 15
 					{{end}}
16 16
 					<div class="content">