mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Avatar refactor, move avatar code from models to models.avatars, remove duplicated code (#17123)
				
					
				
			Why this refactor The goal is to move most files from `models` package to `models.xxx` package. Many models depend on avatar model, so just move this first. And the existing logic is not clear, there are too many function like `AvatarLink`, `RelAvatarLink`, `SizedRelAvatarLink`, `SizedAvatarLink`, `MakeFinalAvatarURL`, `HashedAvatarLink`, etc. This refactor make everything clear: * user.AvatarLink() * user.AvatarLinkWithSize(size) * avatars.GenerateEmailAvatarFastLink(email, size) * avatars.GenerateEmailAvatarFinalLink(email, size) And many duplicated code are deleted in route handler, the handler and the model share the same avatar logic now.
This commit is contained in:
		| @@ -11,7 +11,6 @@ import ( | |||||||
| 	"mime/multipart" | 	"mime/multipart" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"strings" |  | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| @@ -75,14 +74,8 @@ func TestUserAvatar(t *testing.T) { | |||||||
| 		user2 = db.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo3, is an org | 		user2 = db.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo3, is an org | ||||||
|  |  | ||||||
| 		req = NewRequest(t, "GET", user2.AvatarLink()) | 		req = NewRequest(t, "GET", user2.AvatarLink()) | ||||||
| 		resp := session.MakeRequest(t, req, http.StatusFound) | 		_ = session.MakeRequest(t, req, http.StatusOK) | ||||||
| 		location := resp.Header().Get("Location") |  | ||||||
| 		if !strings.HasPrefix(location, "/avatars") { |  | ||||||
| 			assert.Fail(t, "Avatar location is not local: %s", location) |  | ||||||
| 		} |  | ||||||
| 		req = NewRequest(t, "GET", location) |  | ||||||
| 		session.MakeRequest(t, req, http.StatusOK) |  | ||||||
|  |  | ||||||
| 		// Can't test if the response matches because the image is regened on upload but checking that this at least doesn't give a 404 should be enough. | 		// Can't test if the response matches because the image is re-generated on upload but checking that this at least doesn't give a 404 should be enough. | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										148
									
								
								models/avatar.go
									
									
									
									
									
								
							
							
						
						
									
										148
									
								
								models/avatar.go
									
									
									
									
									
								
							| @@ -1,148 +0,0 @@ | |||||||
| // 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 ( |  | ||||||
| 	"context" |  | ||||||
| 	"crypto/md5" |  | ||||||
| 	"fmt" |  | ||||||
| 	"net/url" |  | ||||||
| 	"path" |  | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" |  | ||||||
| 	"code.gitea.io/gitea/modules/base" |  | ||||||
| 	"code.gitea.io/gitea/modules/cache" |  | ||||||
| 	"code.gitea.io/gitea/modules/log" |  | ||||||
| 	"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"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	db.RegisterModel(new(EmailHash)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // DefaultAvatarLink the default avatar link |  | ||||||
| func DefaultAvatarLink() string { |  | ||||||
| 	u, err := url.Parse(setting.AppSubURL) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error("GetUserByEmail: %v", err) |  | ||||||
| 		return "" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	u.Path = path.Join(u.Path, "/assets/img/avatar_default.png") |  | ||||||
| 	return u.String() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // DefaultAvatarSize is a sentinel value for the default avatar size, as |  | ||||||
| // determined by the avatar-hosting service. |  | ||||||
| const DefaultAvatarSize = -1 |  | ||||||
|  |  | ||||||
| // DefaultAvatarPixelSize is the default size in pixels of a rendered avatar |  | ||||||
| const DefaultAvatarPixelSize = 28 |  | ||||||
|  |  | ||||||
| // AvatarRenderedSizeFactor is the factor by which the default size is increased for finer rendering |  | ||||||
| const AvatarRenderedSizeFactor = 4 |  | ||||||
|  |  | ||||||
| // HashEmail hashes email address to MD5 string. |  | ||||||
| // https://en.gravatar.com/site/implement/hash/ |  | ||||||
| func HashEmail(email string) string { |  | ||||||
| 	return base.EncodeMD5(strings.ToLower(strings.TrimSpace(email))) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // 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 := db.GetEngine(db.DefaultContext).Get(&emailHash) |  | ||||||
| 		return emailHash.Email, err |  | ||||||
| 	}) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // 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("LibravatarService.FromEmail(email=%s): error %v", email, err) |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	u, err := url.Parse(urlStr) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error("Failed to parse libravatar url(%s): error %v", urlStr, err) |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return u, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // HashedAvatarLink returns an avatar link for a provided email |  | ||||||
| func HashedAvatarLink(email string, size int) 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, |  | ||||||
| 		} |  | ||||||
| 		// OK we're going to open a session just because I think that that might hide away any problems with postgres reporting errors |  | ||||||
| 		if err := db.WithTx(func(ctx context.Context) error { |  | ||||||
| 			has, err := db.GetEngine(ctx).Where("email = ? AND hash = ?", emailHash.Email, emailHash.Hash).Get(new(EmailHash)) |  | ||||||
| 			if has || err != nil { |  | ||||||
| 				// Seriously we don't care about any DB problems just return the lowerEmail - we expect the transaction to fail most of the time |  | ||||||
| 				return nil |  | ||||||
| 			} |  | ||||||
| 			_, _ = db.GetEngine(ctx).Insert(emailHash) |  | ||||||
| 			return nil |  | ||||||
| 		}); err != nil { |  | ||||||
| 			// Seriously we don't care about any DB problems just return the lowerEmail - we expect the transaction to fail most of the time |  | ||||||
| 			return lowerEmail, nil |  | ||||||
| 		} |  | ||||||
| 		return lowerEmail, nil |  | ||||||
| 	}) |  | ||||||
| 	if size > 0 { |  | ||||||
| 		return setting.AppSubURL + "/avatar/" + url.PathEscape(sum) + "?size=" + strconv.Itoa(size) |  | ||||||
| 	} |  | ||||||
| 	return setting.AppSubURL + "/avatar/" + url.PathEscape(sum) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // MakeFinalAvatarURL constructs the final avatar URL string |  | ||||||
| func MakeFinalAvatarURL(u *url.URL, size int) string { |  | ||||||
| 	vals := u.Query() |  | ||||||
| 	vals.Set("d", "identicon") |  | ||||||
| 	if size != DefaultAvatarSize { |  | ||||||
| 		vals.Set("s", strconv.Itoa(size)) |  | ||||||
| 	} |  | ||||||
| 	u.RawQuery = vals.Encode() |  | ||||||
| 	return u.String() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // 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 { |  | ||||||
| 		// This is the slow path that would need to call LibravatarURL() which |  | ||||||
| 		// does DNS lookups. Avoid it by issuing a redirect so we don't block |  | ||||||
| 		// the template render with network requests. |  | ||||||
| 		return HashedAvatarLink(email, size) |  | ||||||
| 	} 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() |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return MakeFinalAvatarURL(avatarURL, size) |  | ||||||
| } |  | ||||||
							
								
								
									
										180
									
								
								models/avatars/avatar.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								models/avatars/avatar.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | |||||||
|  | // Copyright 2021 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 avatars | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"net/url" | ||||||
|  | 	"path" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models/db" | ||||||
|  | 	"code.gitea.io/gitea/modules/base" | ||||||
|  | 	"code.gitea.io/gitea/modules/cache" | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // DefaultAvatarPixelSize is the default size in pixels of a rendered avatar | ||||||
|  | const DefaultAvatarPixelSize = 28 | ||||||
|  |  | ||||||
|  | // AvatarRenderedSizeFactor is the factor by which the default size is increased for finer rendering | ||||||
|  | const AvatarRenderedSizeFactor = 4 | ||||||
|  |  | ||||||
|  | // EmailHash represents a pre-generated hash map (mainly used by LibravatarURL, it queries email server's DNS records) | ||||||
|  | type EmailHash struct { | ||||||
|  | 	Hash  string `xorm:"pk varchar(32)"` | ||||||
|  | 	Email string `xorm:"UNIQUE NOT NULL"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	db.RegisterModel(new(EmailHash)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DefaultAvatarLink the default avatar link | ||||||
|  | func DefaultAvatarLink() string { | ||||||
|  | 	u, err := url.Parse(setting.AppSubURL) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("GetUserByEmail: %v", err) | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	u.Path = path.Join(u.Path, "/assets/img/avatar_default.png") | ||||||
|  | 	return u.String() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // HashEmail hashes email address to MD5 string. https://en.gravatar.com/site/implement/hash/ | ||||||
|  | func HashEmail(email string) string { | ||||||
|  | 	return base.EncodeMD5(strings.ToLower(strings.TrimSpace(email))) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 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 := db.GetEngine(db.DefaultContext).Get(&emailHash) | ||||||
|  | 		return emailHash.Email, err | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // LibravatarURL returns the URL for the given email. Slow due to the DNS lookup. | ||||||
|  | // 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("LibravatarService.FromEmail(email=%s): error %v", email, err) | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	u, err := url.Parse(urlStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("Failed to parse libravatar url(%s): error %v", urlStr, err) | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return u, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // saveEmailHash returns an avatar link for a provided email, | ||||||
|  | // the email and hash are saved into database, which will be used by GetEmailForHash later | ||||||
|  | func saveEmailHash(email string) string { | ||||||
|  | 	lowerEmail := strings.ToLower(strings.TrimSpace(email)) | ||||||
|  | 	emailHash := HashEmail(lowerEmail) | ||||||
|  | 	_, _ = cache.GetString("Avatar:"+emailHash, func() (string, error) { | ||||||
|  | 		emailHash := &EmailHash{ | ||||||
|  | 			Email: lowerEmail, | ||||||
|  | 			Hash:  emailHash, | ||||||
|  | 		} | ||||||
|  | 		// OK we're going to open a session just because I think that that might hide away any problems with postgres reporting errors | ||||||
|  | 		if err := db.WithTx(func(ctx context.Context) error { | ||||||
|  | 			has, err := db.GetEngine(ctx).Where("email = ? AND hash = ?", emailHash.Email, emailHash.Hash).Get(new(EmailHash)) | ||||||
|  | 			if has || err != nil { | ||||||
|  | 				// Seriously we don't care about any DB problems just return the lowerEmail - we expect the transaction to fail most of the time | ||||||
|  | 				return nil | ||||||
|  | 			} | ||||||
|  | 			_, _ = db.GetEngine(ctx).Insert(emailHash) | ||||||
|  | 			return nil | ||||||
|  | 		}); err != nil { | ||||||
|  | 			// Seriously we don't care about any DB problems just return the lowerEmail - we expect the transaction to fail most of the time | ||||||
|  | 			return lowerEmail, nil | ||||||
|  | 		} | ||||||
|  | 		return lowerEmail, nil | ||||||
|  | 	}) | ||||||
|  | 	return emailHash | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GenerateUserAvatarFastLink returns a fast link (302) to the user's avatar: "/user/avatar/${User.Name}/${size}" | ||||||
|  | func GenerateUserAvatarFastLink(userName string, size int) string { | ||||||
|  | 	if size < 0 { | ||||||
|  | 		size = 0 | ||||||
|  | 	} | ||||||
|  | 	return setting.AppSubURL + "/user/avatar/" + userName + "/" + strconv.Itoa(size) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GenerateUserAvatarImageLink returns a link for `User.Avatar` image file: "/avatars/${User.Avatar}" | ||||||
|  | func GenerateUserAvatarImageLink(userAvatar string, size int) string { | ||||||
|  | 	if size > 0 { | ||||||
|  | 		return setting.AppSubURL + "/avatars/" + userAvatar + "?size=" + strconv.Itoa(size) | ||||||
|  | 	} | ||||||
|  | 	return setting.AppSubURL + "/avatars/" + userAvatar | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // generateRecognizedAvatarURL generate a recognized avatar (Gravatar/Libravatar) URL, it modifies the URL so the parameter is passed by a copy | ||||||
|  | func generateRecognizedAvatarURL(u url.URL, size int) string { | ||||||
|  | 	urlQuery := u.Query() | ||||||
|  | 	urlQuery.Set("d", "identicon") | ||||||
|  | 	if size > 0 { | ||||||
|  | 		urlQuery.Set("s", strconv.Itoa(size)) | ||||||
|  | 	} | ||||||
|  | 	u.RawQuery = urlQuery.Encode() | ||||||
|  | 	return u.String() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // generateEmailAvatarLink returns a email avatar link. | ||||||
|  | // if final is true, it may use a slow path (eg: query DNS). | ||||||
|  | // if final is false, it always uses a fast path. | ||||||
|  | func generateEmailAvatarLink(email string, size int, final bool) string { | ||||||
|  | 	email = strings.TrimSpace(email) | ||||||
|  | 	if email == "" { | ||||||
|  | 		return DefaultAvatarLink() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var err error | ||||||
|  | 	if setting.EnableFederatedAvatar && setting.LibravatarService != nil { | ||||||
|  | 		emailHash := saveEmailHash(email) | ||||||
|  | 		if final { | ||||||
|  | 			// for final link, we can spend more time on slow external query | ||||||
|  | 			var avatarURL *url.URL | ||||||
|  | 			if avatarURL, err = LibravatarURL(email); err != nil { | ||||||
|  | 				return DefaultAvatarLink() | ||||||
|  | 			} | ||||||
|  | 			return generateRecognizedAvatarURL(*avatarURL, size) | ||||||
|  | 		} | ||||||
|  | 		// for non-final link, we should return fast (use a 302 redirection link) | ||||||
|  | 		urlStr := setting.AppSubURL + "/avatar/" + emailHash | ||||||
|  | 		if size > 0 { | ||||||
|  | 			urlStr += "?size=" + strconv.Itoa(size) | ||||||
|  | 		} | ||||||
|  | 		return urlStr | ||||||
|  | 	} else if !setting.DisableGravatar { | ||||||
|  | 		// copy GravatarSourceURL, because we will modify its Path. | ||||||
|  | 		avatarURLCopy := *setting.GravatarSourceURL | ||||||
|  | 		avatarURLCopy.Path = path.Join(avatarURLCopy.Path, HashEmail(email)) | ||||||
|  | 		return generateRecognizedAvatarURL(avatarURLCopy, size) | ||||||
|  | 	} | ||||||
|  | 	return DefaultAvatarLink() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | //GenerateEmailAvatarFastLink returns a avatar link (fast, the link may be a delegated one: "/avatar/${hash}") | ||||||
|  | func GenerateEmailAvatarFastLink(email string, size int) string { | ||||||
|  | 	return generateEmailAvatarLink(email, size, false) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | //GenerateEmailAvatarFinalLink returns a avatar final link (maybe slow) | ||||||
|  | func GenerateEmailAvatarFinalLink(email string, size int) string { | ||||||
|  | 	return generateEmailAvatarLink(email, size, true) | ||||||
|  | } | ||||||
| @@ -2,7 +2,7 @@ | |||||||
| // Use of this source code is governed by a MIT-style | // Use of this source code is governed by a MIT-style | ||||||
| // license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. | ||||||
| 
 | 
 | ||||||
| package models | package avatars | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"net/url" | 	"net/url" | ||||||
| @@ -44,11 +44,11 @@ func TestSizedAvatarLink(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 	disableGravatar() | 	disableGravatar() | ||||||
| 	assert.Equal(t, "/testsuburl/assets/img/avatar_default.png", | 	assert.Equal(t, "/testsuburl/assets/img/avatar_default.png", | ||||||
| 		SizedAvatarLink("gitea@example.com", 100)) | 		GenerateEmailAvatarFastLink("gitea@example.com", 100)) | ||||||
| 
 | 
 | ||||||
| 	enableGravatar(t) | 	enableGravatar(t) | ||||||
| 	assert.Equal(t, | 	assert.Equal(t, | ||||||
| 		"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100", | 		"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100", | ||||||
| 		SizedAvatarLink("gitea@example.com", 100), | 		GenerateEmailAvatarFastLink("gitea@example.com", 100), | ||||||
| 	) | 	) | ||||||
| } | } | ||||||
| @@ -9,9 +9,8 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"image/png" | 	"image/png" | ||||||
| 	"io" | 	"io" | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models/avatars" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/modules/avatar" | 	"code.gitea.io/gitea/modules/avatar" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| @@ -40,7 +39,7 @@ func (u *User) generateRandomAvatar(e db.Engine) error { | |||||||
| 		return fmt.Errorf("RandomImage: %v", err) | 		return fmt.Errorf("RandomImage: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	u.Avatar = HashEmail(seed) | 	u.Avatar = avatars.HashEmail(seed) | ||||||
|  |  | ||||||
| 	// Don't share the images so that we can delete them easily | 	// Don't share the images so that we can delete them easily | ||||||
| 	if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { | 	if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { | ||||||
| @@ -60,61 +59,41 @@ func (u *User) generateRandomAvatar(e db.Engine) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // SizedRelAvatarLink returns a link to the user's avatar via | // AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size | ||||||
| // the local explore page. Function returns immediately. | func (u *User) AvatarLinkWithSize(size int) string { | ||||||
| // When applicable, the link is for an avatar of the indicated size (in pixels). |  | ||||||
| func (u *User) SizedRelAvatarLink(size int) string { |  | ||||||
| 	return setting.AppSubURL + "/user/avatar/" + u.Name + "/" + strconv.Itoa(size) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // RealSizedAvatarLink returns a link to the user's avatar. When |  | ||||||
| // applicable, the link is for an avatar of the indicated size (in pixels). |  | ||||||
| // |  | ||||||
| // This function make take time to return when federated avatars |  | ||||||
| // are in use, due to a DNS lookup need |  | ||||||
| // |  | ||||||
| func (u *User) RealSizedAvatarLink(size int) string { |  | ||||||
| 	if u.ID == -1 { | 	if u.ID == -1 { | ||||||
| 		return DefaultAvatarLink() | 		// ghost user | ||||||
|  | 		return avatars.DefaultAvatarLink() | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	useLocalAvatar := false | ||||||
|  | 	autoGenerateAvatar := false | ||||||
|  |  | ||||||
| 	switch { | 	switch { | ||||||
| 	case u.UseCustomAvatar: | 	case u.UseCustomAvatar: | ||||||
| 		if u.Avatar == "" { | 		useLocalAvatar = true | ||||||
| 			return DefaultAvatarLink() |  | ||||||
| 		} |  | ||||||
| 		if size > 0 { |  | ||||||
| 			return setting.AppSubURL + "/avatars/" + u.Avatar + "?size=" + strconv.Itoa(size) |  | ||||||
| 		} |  | ||||||
| 		return setting.AppSubURL + "/avatars/" + u.Avatar |  | ||||||
| 	case setting.DisableGravatar, setting.OfflineMode: | 	case setting.DisableGravatar, setting.OfflineMode: | ||||||
| 		if u.Avatar == "" { | 		useLocalAvatar = true | ||||||
|  | 		autoGenerateAvatar = true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if useLocalAvatar { | ||||||
|  | 		if u.Avatar == "" && autoGenerateAvatar { | ||||||
| 			if err := u.GenerateRandomAvatar(); err != nil { | 			if err := u.GenerateRandomAvatar(); err != nil { | ||||||
| 				log.Error("GenerateRandomAvatar: %v", err) | 				log.Error("GenerateRandomAvatar: %v", err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		if size > 0 { | 		if u.Avatar == "" { | ||||||
| 			return setting.AppSubURL + "/avatars/" + u.Avatar + "?size=" + strconv.Itoa(size) | 			return avatars.DefaultAvatarLink() | ||||||
| 		} | 		} | ||||||
| 		return setting.AppSubURL + "/avatars/" + u.Avatar | 		return avatars.GenerateUserAvatarImageLink(u.Avatar, size) | ||||||
| 	} | 	} | ||||||
| 	return SizedAvatarLink(u.AvatarEmail, size) | 	return avatars.GenerateEmailAvatarFastLink(u.AvatarEmail, size) | ||||||
| } | } | ||||||
|  |  | ||||||
| // RelAvatarLink returns a relative link to the user's avatar. The link | // AvatarLink returns a avatar link with default size | ||||||
| // 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(DefaultAvatarSize) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // AvatarLink returns user avatar absolute link. |  | ||||||
| func (u *User) AvatarLink() string { | func (u *User) AvatarLink() string { | ||||||
| 	link := u.RelAvatarLink() | 	return u.AvatarLinkWithSize(0) | ||||||
| 	if link[0] == '/' && link[1] != '/' { |  | ||||||
| 		return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL)[1:] |  | ||||||
| 	} |  | ||||||
| 	return link |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // UploadAvatar saves custom avatar for user. | // UploadAvatar saves custom avatar for user. | ||||||
|   | |||||||
| @@ -16,12 +16,17 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // GetCacheControl returns a suitable "Cache-Control" header value | // AddCacheControlToHeader adds suitable cache-control headers to response | ||||||
| func GetCacheControl() string { | func AddCacheControlToHeader(h http.Header, d time.Duration) { | ||||||
| 	if !setting.IsProd() { | 	if setting.IsProd() { | ||||||
| 		return "no-store" | 		h.Set("Cache-Control", "private, max-age="+strconv.Itoa(int(d.Seconds()))) | ||||||
|  | 	} else { | ||||||
|  | 		h.Set("Cache-Control", "no-store") | ||||||
|  | 		// to remind users they are using non-prod setting. | ||||||
|  | 		// some users may be confused by "Cache-Control: no-store" in their setup if they did wrong to `RUN_MODE` in `app.ini`. | ||||||
|  | 		h.Add("X-Gitea-Debug", "RUN_MODE="+setting.RunMode) | ||||||
|  | 		h.Add("X-Gitea-Debug", "CacheControl=no-store") | ||||||
| 	} | 	} | ||||||
| 	return "private, max-age=" + strconv.FormatInt(int64(setting.StaticCacheTime.Seconds()), 10) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // generateETag generates an ETag based on size, filename and file modification time | // generateETag generates an ETag based on size, filename and file modification time | ||||||
| @@ -32,7 +37,7 @@ func generateETag(fi os.FileInfo) string { | |||||||
|  |  | ||||||
| // HandleTimeCache handles time-based caching for a HTTP request | // HandleTimeCache handles time-based caching for a HTTP request | ||||||
| func HandleTimeCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) { | func HandleTimeCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) { | ||||||
| 	w.Header().Set("Cache-Control", GetCacheControl()) | 	AddCacheControlToHeader(w.Header(), setting.StaticCacheTime) | ||||||
|  |  | ||||||
| 	ifModifiedSince := req.Header.Get("If-Modified-Since") | 	ifModifiedSince := req.Header.Get("If-Modified-Since") | ||||||
| 	if ifModifiedSince != "" { | 	if ifModifiedSince != "" { | ||||||
| @@ -63,7 +68,7 @@ func HandleGenericETagCache(req *http.Request, w http.ResponseWriter, etag strin | |||||||
| 			return true | 			return true | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	w.Header().Set("Cache-Control", GetCacheControl()) | 	AddCacheControlToHeader(w.Header(), setting.StaticCacheTime) | ||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| @@ -24,6 +25,17 @@ func (m mockFileInfo) ModTime() time.Time { return time.Time{} } | |||||||
| func (m mockFileInfo) IsDir() bool        { return false } | func (m mockFileInfo) IsDir() bool        { return false } | ||||||
| func (m mockFileInfo) Sys() interface{}   { return nil } | func (m mockFileInfo) Sys() interface{}   { return nil } | ||||||
|  |  | ||||||
|  | func countFormalHeaders(h http.Header) (c int) { | ||||||
|  | 	for k := range h { | ||||||
|  | 		// ignore our headers for internal usage | ||||||
|  | 		if strings.HasPrefix(k, "X-Gitea-") { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		c++ | ||||||
|  | 	} | ||||||
|  | 	return c | ||||||
|  | } | ||||||
|  |  | ||||||
| func TestHandleFileETagCache(t *testing.T) { | func TestHandleFileETagCache(t *testing.T) { | ||||||
| 	fi := mockFileInfo{} | 	fi := mockFileInfo{} | ||||||
| 	etag := `"MTBnaXRlYS50ZXN0TW9uLCAwMSBKYW4gMDAwMSAwMDowMDowMCBHTVQ="` | 	etag := `"MTBnaXRlYS50ZXN0TW9uLCAwMSBKYW4gMDAwMSAwMDowMDowMCBHTVQ="` | ||||||
| @@ -35,7 +47,7 @@ func TestHandleFileETagCache(t *testing.T) { | |||||||
| 		handled := HandleFileETagCache(req, w, fi) | 		handled := HandleFileETagCache(req, w, fi) | ||||||
|  |  | ||||||
| 		assert.False(t, handled) | 		assert.False(t, handled) | ||||||
| 		assert.Len(t, w.Header(), 2) | 		assert.Equal(t, 2, countFormalHeaders(w.Header())) | ||||||
| 		assert.Contains(t, w.Header(), "Cache-Control") | 		assert.Contains(t, w.Header(), "Cache-Control") | ||||||
| 		assert.Contains(t, w.Header(), "Etag") | 		assert.Contains(t, w.Header(), "Etag") | ||||||
| 		assert.Equal(t, etag, w.Header().Get("Etag")) | 		assert.Equal(t, etag, w.Header().Get("Etag")) | ||||||
| @@ -49,7 +61,7 @@ func TestHandleFileETagCache(t *testing.T) { | |||||||
| 		handled := HandleFileETagCache(req, w, fi) | 		handled := HandleFileETagCache(req, w, fi) | ||||||
|  |  | ||||||
| 		assert.False(t, handled) | 		assert.False(t, handled) | ||||||
| 		assert.Len(t, w.Header(), 2) | 		assert.Equal(t, 2, countFormalHeaders(w.Header())) | ||||||
| 		assert.Contains(t, w.Header(), "Cache-Control") | 		assert.Contains(t, w.Header(), "Cache-Control") | ||||||
| 		assert.Contains(t, w.Header(), "Etag") | 		assert.Contains(t, w.Header(), "Etag") | ||||||
| 		assert.Equal(t, etag, w.Header().Get("Etag")) | 		assert.Equal(t, etag, w.Header().Get("Etag")) | ||||||
| @@ -63,7 +75,7 @@ func TestHandleFileETagCache(t *testing.T) { | |||||||
| 		handled := HandleFileETagCache(req, w, fi) | 		handled := HandleFileETagCache(req, w, fi) | ||||||
|  |  | ||||||
| 		assert.True(t, handled) | 		assert.True(t, handled) | ||||||
| 		assert.Len(t, w.Header(), 1) | 		assert.Equal(t, 1, countFormalHeaders(w.Header())) | ||||||
| 		assert.Contains(t, w.Header(), "Etag") | 		assert.Contains(t, w.Header(), "Etag") | ||||||
| 		assert.Equal(t, etag, w.Header().Get("Etag")) | 		assert.Equal(t, etag, w.Header().Get("Etag")) | ||||||
| 		assert.Equal(t, http.StatusNotModified, w.Code) | 		assert.Equal(t, http.StatusNotModified, w.Code) | ||||||
| @@ -80,7 +92,7 @@ func TestHandleGenericETagCache(t *testing.T) { | |||||||
| 		handled := HandleGenericETagCache(req, w, etag) | 		handled := HandleGenericETagCache(req, w, etag) | ||||||
|  |  | ||||||
| 		assert.False(t, handled) | 		assert.False(t, handled) | ||||||
| 		assert.Len(t, w.Header(), 2) | 		assert.Equal(t, 2, countFormalHeaders(w.Header())) | ||||||
| 		assert.Contains(t, w.Header(), "Cache-Control") | 		assert.Contains(t, w.Header(), "Cache-Control") | ||||||
| 		assert.Contains(t, w.Header(), "Etag") | 		assert.Contains(t, w.Header(), "Etag") | ||||||
| 		assert.Equal(t, etag, w.Header().Get("Etag")) | 		assert.Equal(t, etag, w.Header().Get("Etag")) | ||||||
| @@ -94,7 +106,7 @@ func TestHandleGenericETagCache(t *testing.T) { | |||||||
| 		handled := HandleGenericETagCache(req, w, etag) | 		handled := HandleGenericETagCache(req, w, etag) | ||||||
|  |  | ||||||
| 		assert.False(t, handled) | 		assert.False(t, handled) | ||||||
| 		assert.Len(t, w.Header(), 2) | 		assert.Equal(t, 2, countFormalHeaders(w.Header())) | ||||||
| 		assert.Contains(t, w.Header(), "Cache-Control") | 		assert.Contains(t, w.Header(), "Cache-Control") | ||||||
| 		assert.Contains(t, w.Header(), "Etag") | 		assert.Contains(t, w.Header(), "Etag") | ||||||
| 		assert.Equal(t, etag, w.Header().Get("Etag")) | 		assert.Equal(t, etag, w.Header().Get("Etag")) | ||||||
| @@ -108,7 +120,7 @@ func TestHandleGenericETagCache(t *testing.T) { | |||||||
| 		handled := HandleGenericETagCache(req, w, etag) | 		handled := HandleGenericETagCache(req, w, etag) | ||||||
|  |  | ||||||
| 		assert.True(t, handled) | 		assert.True(t, handled) | ||||||
| 		assert.Len(t, w.Header(), 1) | 		assert.Equal(t, 1, countFormalHeaders(w.Header())) | ||||||
| 		assert.Contains(t, w.Header(), "Etag") | 		assert.Contains(t, w.Header(), "Etag") | ||||||
| 		assert.Equal(t, etag, w.Header().Get("Etag")) | 		assert.Equal(t, etag, w.Header().Get("Etag")) | ||||||
| 		assert.Equal(t, http.StatusNotModified, w.Code) | 		assert.Equal(t, http.StatusNotModified, w.Code) | ||||||
| @@ -122,7 +134,7 @@ func TestHandleGenericETagCache(t *testing.T) { | |||||||
| 		handled := HandleGenericETagCache(req, w, etag) | 		handled := HandleGenericETagCache(req, w, etag) | ||||||
|  |  | ||||||
| 		assert.False(t, handled) | 		assert.False(t, handled) | ||||||
| 		assert.Len(t, w.Header(), 2) | 		assert.Equal(t, 2, countFormalHeaders(w.Header())) | ||||||
| 		assert.Contains(t, w.Header(), "Cache-Control") | 		assert.Contains(t, w.Header(), "Cache-Control") | ||||||
| 		assert.Contains(t, w.Header(), "Etag") | 		assert.Contains(t, w.Header(), "Etag") | ||||||
| 		assert.Equal(t, etag, w.Header().Get("Etag")) | 		assert.Equal(t, etag, w.Header().Get("Etag")) | ||||||
| @@ -136,7 +148,7 @@ func TestHandleGenericETagCache(t *testing.T) { | |||||||
| 		handled := HandleGenericETagCache(req, w, etag) | 		handled := HandleGenericETagCache(req, w, etag) | ||||||
|  |  | ||||||
| 		assert.True(t, handled) | 		assert.True(t, handled) | ||||||
| 		assert.Len(t, w.Header(), 1) | 		assert.Equal(t, 1, countFormalHeaders(w.Header())) | ||||||
| 		assert.Contains(t, w.Header(), "Etag") | 		assert.Contains(t, w.Header(), "Etag") | ||||||
| 		assert.Equal(t, etag, w.Header().Get("Etag")) | 		assert.Equal(t, etag, w.Header().Get("Etag")) | ||||||
| 		assert.Equal(t, http.StatusNotModified, w.Code) | 		assert.Equal(t, http.StatusNotModified, w.Code) | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
|  | 	"code.gitea.io/gitea/models/avatars" | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	api "code.gitea.io/gitea/modules/structs" | 	api "code.gitea.io/gitea/modules/structs" | ||||||
| @@ -139,14 +140,14 @@ func (pc *PushCommits) AvatarLink(email string) string { | |||||||
| 		return avatar | 		return avatar | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	size := models.DefaultAvatarPixelSize * models.AvatarRenderedSizeFactor | 	size := avatars.DefaultAvatarPixelSize * avatars.AvatarRenderedSizeFactor | ||||||
|  |  | ||||||
| 	u, ok := pc.emailUsers[email] | 	u, ok := pc.emailUsers[email] | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		var err error | 		var err error | ||||||
| 		u, err = models.GetUserByEmail(email) | 		u, err = models.GetUserByEmail(email) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			pc.avatars[email] = models.SizedAvatarLink(email, size) | 			pc.avatars[email] = avatars.GenerateEmailAvatarFastLink(email, size) | ||||||
| 			if !models.IsErrUserNotExist(err) { | 			if !models.IsErrUserNotExist(err) { | ||||||
| 				log.Error("GetUserByEmail: %v", err) | 				log.Error("GetUserByEmail: %v", err) | ||||||
| 				return "" | 				return "" | ||||||
| @@ -156,7 +157,7 @@ func (pc *PushCommits) AvatarLink(email string) string { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	if u != nil { | 	if u != nil { | ||||||
| 		pc.avatars[email] = u.RealSizedAvatarLink(size) | 		pc.avatars[email] = u.AvatarLinkWithSize(size) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return pc.avatars[email] | 	return pc.avatars[email] | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ import ( | |||||||
| 	"unicode" | 	"unicode" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
|  | 	"code.gitea.io/gitea/models/avatars" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/emoji" | 	"code.gitea.io/gitea/modules/emoji" | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| @@ -550,16 +551,16 @@ func SVG(icon string, others ...interface{}) template.HTML { | |||||||
|  |  | ||||||
| // Avatar renders user avatars. args: user, size (int), class (string) | // Avatar renders user avatars. args: user, size (int), class (string) | ||||||
| func Avatar(item interface{}, others ...interface{}) template.HTML { | func Avatar(item interface{}, others ...interface{}) template.HTML { | ||||||
| 	size, class := parseOthers(models.DefaultAvatarPixelSize, "ui avatar image", others...) | 	size, class := parseOthers(avatars.DefaultAvatarPixelSize, "ui avatar image", others...) | ||||||
|  |  | ||||||
| 	if user, ok := item.(*models.User); ok { | 	if user, ok := item.(*models.User); ok { | ||||||
| 		src := user.RealSizedAvatarLink(size * models.AvatarRenderedSizeFactor) | 		src := user.AvatarLinkWithSize(size * avatars.AvatarRenderedSizeFactor) | ||||||
| 		if src != "" { | 		if src != "" { | ||||||
| 			return AvatarHTML(src, size, class, user.DisplayName()) | 			return AvatarHTML(src, size, class, user.DisplayName()) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	if user, ok := item.(*models.Collaborator); ok { | 	if user, ok := item.(*models.Collaborator); ok { | ||||||
| 		src := user.RealSizedAvatarLink(size * models.AvatarRenderedSizeFactor) | 		src := user.AvatarLinkWithSize(size * avatars.AvatarRenderedSizeFactor) | ||||||
| 		if src != "" { | 		if src != "" { | ||||||
| 			return AvatarHTML(src, size, class, user.DisplayName()) | 			return AvatarHTML(src, size, class, user.DisplayName()) | ||||||
| 		} | 		} | ||||||
| @@ -575,7 +576,7 @@ func AvatarByAction(action *models.Action, others ...interface{}) template.HTML | |||||||
|  |  | ||||||
| // RepoAvatar renders repo avatars. args: repo, size(int), class (string) | // RepoAvatar renders repo avatars. args: repo, size(int), class (string) | ||||||
| func RepoAvatar(repo *models.Repository, others ...interface{}) template.HTML { | func RepoAvatar(repo *models.Repository, others ...interface{}) template.HTML { | ||||||
| 	size, class := parseOthers(models.DefaultAvatarPixelSize, "ui avatar image", others...) | 	size, class := parseOthers(avatars.DefaultAvatarPixelSize, "ui avatar image", others...) | ||||||
|  |  | ||||||
| 	src := repo.RelAvatarLink() | 	src := repo.RelAvatarLink() | ||||||
| 	if src != "" { | 	if src != "" { | ||||||
| @@ -586,8 +587,8 @@ func RepoAvatar(repo *models.Repository, others ...interface{}) template.HTML { | |||||||
|  |  | ||||||
| // AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string) | // AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string) | ||||||
| func AvatarByEmail(email string, name string, others ...interface{}) template.HTML { | func AvatarByEmail(email string, name string, others ...interface{}) template.HTML { | ||||||
| 	size, class := parseOthers(models.DefaultAvatarPixelSize, "ui avatar image", others...) | 	size, class := parseOthers(avatars.DefaultAvatarPixelSize, "ui avatar image", others...) | ||||||
| 	src := models.SizedAvatarLink(email, size*models.AvatarRenderedSizeFactor) | 	src := avatars.GenerateEmailAvatarFastLink(email, size*avatars.AvatarRenderedSizeFactor) | ||||||
|  |  | ||||||
| 	if src != "" { | 	if src != "" { | ||||||
| 		return AvatarHTML(src, size, class, name) | 		return AvatarHTML(src, size, class, name) | ||||||
|   | |||||||
| @@ -2614,5 +2614,5 @@ func handleTeamMentions(ctx *context.Context) { | |||||||
|  |  | ||||||
| 	ctx.Data["MentionableTeams"] = ctx.Repo.Owner.Teams | 	ctx.Data["MentionableTeams"] = ctx.Repo.Owner.Teams | ||||||
| 	ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name | 	ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name | ||||||
| 	ctx.Data["MentionableTeamsOrgAvatar"] = ctx.Repo.Owner.RelAvatarLink() | 	ctx.Data["MentionableTeamsOrgAvatar"] = ctx.Repo.Owner.AvatarLink() | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,100 +5,50 @@ | |||||||
| package user | package user | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" |  | ||||||
| 	"net/url" |  | ||||||
| 	"path" |  | ||||||
| 	"strconv" |  | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
|  | 	"code.gitea.io/gitea/models/avatars" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
| 	"code.gitea.io/gitea/modules/httpcache" | 	"code.gitea.io/gitea/modules/httpcache" | ||||||
| 	"code.gitea.io/gitea/modules/log" |  | ||||||
| 	"code.gitea.io/gitea/modules/setting" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func cacheableRedirect(ctx *context.Context, location string) { | func cacheableRedirect(ctx *context.Context, location string) { | ||||||
| 	ctx.Resp.Header().Set("Cache-Control", httpcache.GetCacheControl()) | 	// here we should not use `setting.StaticCacheTime`, it is pretty long (default: 6 hours) | ||||||
|  | 	// we must make sure the redirection cache time is short enough, otherwise a user won't see the updated avatar in 6 hours | ||||||
|  | 	// it's OK to make the cache time short, it is only a redirection, and doesn't cost much to make a new request | ||||||
|  | 	httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 5*time.Minute) | ||||||
| 	ctx.Redirect(location) | 	ctx.Redirect(location) | ||||||
| } | } | ||||||
|  |  | ||||||
| // Avatar redirect browser to user avatar of requested size | // AvatarByUserName redirect browser to user avatar of requested size | ||||||
| func Avatar(ctx *context.Context) { | func AvatarByUserName(ctx *context.Context) { | ||||||
| 	userName := ctx.Params(":username") | 	userName := ctx.Params(":username") | ||||||
| 	size, err := strconv.Atoi(ctx.Params(":size")) | 	size := int(ctx.ParamsInt64(":size")) | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.ServerError("Invalid avatar size", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	log.Debug("Asked avatar for user %v and size %v", userName, size) |  | ||||||
|  |  | ||||||
| 	var user *models.User | 	var user *models.User | ||||||
| 	if strings.ToLower(userName) != "ghost" { | 	if strings.ToLower(userName) != "ghost" { | ||||||
| 		user, err = models.GetUserByName(userName) | 		var err error | ||||||
| 		if err != nil { | 		if user, err = models.GetUserByName(userName); err != nil { | ||||||
| 			if models.IsErrUserNotExist(err) { | 			ctx.ServerError("Invalid user: "+userName, err) | ||||||
| 				ctx.ServerError("Requested avatar for invalid user", err) |  | ||||||
| 			} else { |  | ||||||
| 				ctx.ServerError("Retrieving user by name", err) |  | ||||||
| 			} |  | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		user = models.NewGhostUser() | 		user = models.NewGhostUser() | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	cacheableRedirect(ctx, user.RealSizedAvatarLink(size)) | 	cacheableRedirect(ctx, user.AvatarLinkWithSize(size)) | ||||||
| } | } | ||||||
|  |  | ||||||
| // AvatarByEmailHash redirects the browser to the appropriate Avatar link | // AvatarByEmailHash redirects the browser to the email avatar link | ||||||
| func AvatarByEmailHash(ctx *context.Context) { | func AvatarByEmailHash(ctx *context.Context) { | ||||||
| 	var err error |  | ||||||
|  |  | ||||||
| 	hash := ctx.Params(":hash") | 	hash := ctx.Params(":hash") | ||||||
| 	if len(hash) == 0 { | 	email, err := avatars.GetEmailForHash(hash) | ||||||
| 		ctx.ServerError("invalid avatar hash", errors.New("hash cannot be empty")) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var email string |  | ||||||
| 	email, err = models.GetEmailForHash(hash) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.ServerError("invalid avatar hash", err) | 		ctx.ServerError("invalid avatar hash: "+hash, err) | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	if len(email) == 0 { |  | ||||||
| 		cacheableRedirect(ctx, models.DefaultAvatarLink()) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	size := ctx.FormInt("size") | 	size := ctx.FormInt("size") | ||||||
| 	if size == 0 { | 	cacheableRedirect(ctx, avatars.GenerateEmailAvatarFinalLink(email, size)) | ||||||
| 		size = models.DefaultAvatarSize |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var avatarURL *url.URL |  | ||||||
|  |  | ||||||
| 	if setting.EnableFederatedAvatar && setting.LibravatarService != nil { |  | ||||||
| 		avatarURL, err = models.LibravatarURL(email) |  | ||||||
| 		if err != nil { |  | ||||||
| 			avatarURL, err = url.Parse(models.DefaultAvatarLink()) |  | ||||||
| 			if err != nil { |  | ||||||
| 				ctx.ServerError("invalid default avatar url", err) |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} else if !setting.DisableGravatar { |  | ||||||
| 		copyOfGravatarSourceURL := *setting.GravatarSourceURL |  | ||||||
| 		avatarURL = ©OfGravatarSourceURL |  | ||||||
| 		avatarURL.Path = path.Join(avatarURL.Path, hash) |  | ||||||
| 	} else { |  | ||||||
| 		avatarURL, err = url.Parse(models.DefaultAvatarLink()) |  | ||||||
| 		if err != nil { |  | ||||||
| 			ctx.ServerError("invalid default avatar url", err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	cacheableRedirect(ctx, models.MakeFinalAvatarURL(avatarURL, size)) |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -366,7 +366,7 @@ func RegisterRoutes(m *web.Route) { | |||||||
| 		m.Get("/activate", user.Activate, reqSignIn) | 		m.Get("/activate", user.Activate, reqSignIn) | ||||||
| 		m.Post("/activate", user.ActivatePost, reqSignIn) | 		m.Post("/activate", user.ActivatePost, reqSignIn) | ||||||
| 		m.Any("/activate_email", user.ActivateEmail) | 		m.Any("/activate_email", user.ActivateEmail) | ||||||
| 		m.Get("/avatar/{username}/{size}", user.Avatar) | 		m.Get("/avatar/{username}/{size}", user.AvatarByUserName) | ||||||
| 		m.Get("/email2user", user.Email2User) | 		m.Get("/email2user", user.Email2User) | ||||||
| 		m.Get("/recover_account", user.ResetPasswd) | 		m.Get("/recover_account", user.ResetPasswd) | ||||||
| 		m.Post("/recover_account", user.ResetPasswdPost) | 		m.Post("/recover_account", user.ResetPasswdPost) | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
|  | 	"code.gitea.io/gitea/models/avatars" | ||||||
| 	"code.gitea.io/gitea/models/login" | 	"code.gitea.io/gitea/models/login" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| @@ -193,7 +194,7 @@ func (s *SSPI) newUser(username string, cfg *sspi.Source) (*models.User, error) | |||||||
| 		IsActive:                     cfg.AutoActivateUsers, | 		IsActive:                     cfg.AutoActivateUsers, | ||||||
| 		Language:                     cfg.DefaultLanguage, | 		Language:                     cfg.DefaultLanguage, | ||||||
| 		UseCustomAvatar:              true, | 		UseCustomAvatar:              true, | ||||||
| 		Avatar:                       models.DefaultAvatarLink(), | 		Avatar:                       avatars.DefaultAvatarLink(), | ||||||
| 		EmailNotificationsPreference: models.EmailNotificationsDisabled, | 		EmailNotificationsPreference: models.EmailNotificationsDisabled, | ||||||
| 	} | 	} | ||||||
| 	if err := models.CreateUser(user); err != nil { | 	if err := models.CreateUser(user); err != nil { | ||||||
|   | |||||||
| @@ -48,11 +48,11 @@ | |||||||
| 			tributeValues: Array.from(new Map([ | 			tributeValues: Array.from(new Map([ | ||||||
| 				{{ range .Participants }} | 				{{ range .Participants }} | ||||||
| 				['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}', | 				['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}', | ||||||
| 				name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.RelAvatarLink}}'}], | 				name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink}}'}], | ||||||
| 				{{ end }} | 				{{ end }} | ||||||
| 				{{ range .Assignees }} | 				{{ range .Assignees }} | ||||||
| 				['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}', | 				['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}', | ||||||
| 				name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.RelAvatarLink}}'}], | 				name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink}}'}], | ||||||
| 				{{ end }} | 				{{ end }} | ||||||
| 				{{ range .MentionableTeams }} | 				{{ range .MentionableTeams }} | ||||||
| 					['{{$.MentionableTeamsOrg}}/{{.Name}}', {key: '{{$.MentionableTeamsOrg}}/{{.Name}}', value: '{{$.MentionableTeamsOrg}}/{{.Name}}', | 					['{{$.MentionableTeamsOrg}}/{{.Name}}', {key: '{{$.MentionableTeamsOrg}}/{{.Name}}', value: '{{$.MentionableTeamsOrg}}/{{.Name}}', | ||||||
|   | |||||||
| @@ -746,7 +746,7 @@ | |||||||
| 		<div class="timeline-item-group"> | 		<div class="timeline-item-group"> | ||||||
| 			<div class="timeline-item event" id="{{.HashTag}}"> | 			<div class="timeline-item event" id="{{.HashTag}}"> | ||||||
| 				<a class="timeline-avatar"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}> | 				<a class="timeline-avatar"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}> | ||||||
| 					<img src="{{.Poster.RelAvatarLink}}"> | 					<img src="{{.Poster.AvatarLink}}"> | ||||||
| 				</a> | 				</a> | ||||||
| 				<span class="badge grey">{{svg "octicon-x" 16}}</span> | 				<span class="badge grey">{{svg "octicon-x" 16}}</span> | ||||||
| 				<span class="text grey"> | 				<span class="text grey"> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user