Fix user avatar (#33439)

release/v1.23
wxiaoguang 23 hours ago committed by GitHub
parent b6fd8741ee
commit a8eaf43f97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 21
      models/user/avatar.go
  2. 40
      models/user/avatar_test.go
  3. 20
      models/user/user_system.go
  4. 32
      models/user/user_system_test.go
  5. 4
      modules/storage/storage.go
  6. 28
      routers/web/user/avatar.go
  7. 2
      routers/web/user/home.go
  8. 2
      routers/web/web.go
  9. 2
      services/actions/notifier_helper.go

@ -38,27 +38,30 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
u.Avatar = avatars.HashEmail(seed) u.Avatar = avatars.HashEmail(seed)
// Don't share the images so that we can delete them easily _, err = storage.Avatars.Stat(u.CustomAvatarRelativePath())
if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { if err != nil {
if err := png.Encode(w, img); err != nil { // If unable to Stat the avatar file (usually it means non-existing), then try to save a new one
log.Error("Encode: %v", err) // 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 := png.Encode(w, img); err != nil {
log.Error("Encode: %v", err)
}
return nil
}); err != nil {
return fmt.Errorf("failed to save avatar %s: %w", u.CustomAvatarRelativePath(), err)
} }
return err
}); err != nil {
return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err)
} }
if _, err := db.GetEngine(ctx).ID(u.ID).Cols("avatar").Update(u); err != nil { if _, err := db.GetEngine(ctx).ID(u.ID).Cols("avatar").Update(u); err != nil {
return err return err
} }
log.Info("New random avatar created: %d", u.ID)
return nil return nil
} }
// AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size // AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size
func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string { func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
if u.IsGhost() { if u.IsGhost() || u.IsGiteaActions() {
return avatars.DefaultAvatarLink() return avatars.DefaultAvatarLink()
} }

@ -4,13 +4,19 @@
package user package user
import ( import (
"context"
"io"
"strings"
"testing" "testing"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestUserAvatarLink(t *testing.T) { func TestUserAvatarLink(t *testing.T) {
@ -26,3 +32,37 @@ func TestUserAvatarLink(t *testing.T) {
link = u.AvatarLink(db.DefaultContext) link = u.AvatarLink(db.DefaultContext)
assert.Equal(t, "https://localhost/sub-path/avatars/avatar.png", link) assert.Equal(t, "https://localhost/sub-path/avatars/avatar.png", link)
} }
func TestUserAvatarGenerate(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
var err error
tmpDir := t.TempDir()
storage.Avatars, err = storage.NewLocalStorage(context.Background(), &setting.Storage{Path: tmpDir})
require.NoError(t, err)
u := unittest.AssertExistsAndLoadBean(t, &User{ID: 2})
// there was no avatar, generate a new one
assert.Empty(t, u.Avatar)
err = GenerateRandomAvatar(db.DefaultContext, u)
require.NoError(t, err)
assert.NotEmpty(t, u.Avatar)
// make sure the generated one exists
oldAvatarPath := u.CustomAvatarRelativePath()
_, err = storage.Avatars.Stat(u.CustomAvatarRelativePath())
require.NoError(t, err)
// and try to change its content
_, err = storage.Avatars.Save(u.CustomAvatarRelativePath(), strings.NewReader("abcd"), 4)
require.NoError(t, err)
// try to generate again
err = GenerateRandomAvatar(db.DefaultContext, u)
require.NoError(t, err)
assert.Equal(t, oldAvatarPath, u.CustomAvatarRelativePath())
f, err := storage.Avatars.Open(u.CustomAvatarRelativePath())
require.NoError(t, err)
defer f.Close()
content, _ := io.ReadAll(f)
assert.Equal(t, "abcd", string(content))
}

@ -24,6 +24,10 @@ func NewGhostUser() *User {
} }
} }
func IsGhostUserName(name string) bool {
return strings.EqualFold(name, GhostUserName)
}
// IsGhost check if user is fake user for a deleted account // IsGhost check if user is fake user for a deleted account
func (u *User) IsGhost() bool { func (u *User) IsGhost() bool {
if u == nil { if u == nil {
@ -48,6 +52,10 @@ const (
ActionsEmail = "teabot@gitea.io" ActionsEmail = "teabot@gitea.io"
) )
func IsGiteaActionsUserName(name string) bool {
return strings.EqualFold(name, ActionsUserName)
}
// NewActionsUser creates and returns a fake user for running the actions. // NewActionsUser creates and returns a fake user for running the actions.
func NewActionsUser() *User { func NewActionsUser() *User {
return &User{ return &User{
@ -65,6 +73,16 @@ func NewActionsUser() *User {
} }
} }
func (u *User) IsActions() bool { func (u *User) IsGiteaActions() bool {
return u != nil && u.ID == ActionsUserID return u != nil && u.ID == ActionsUserID
} }
func GetSystemUserByName(name string) *User {
if IsGhostUserName(name) {
return NewGhostUser()
}
if IsGiteaActionsUserName(name) {
return NewActionsUser()
}
return nil
}

@ -0,0 +1,32 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"testing"
"code.gitea.io/gitea/models/db"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSystemUser(t *testing.T) {
u, err := GetPossibleUserByID(db.DefaultContext, -1)
require.NoError(t, err)
assert.Equal(t, "Ghost", u.Name)
assert.Equal(t, "ghost", u.LowerName)
assert.True(t, u.IsGhost())
assert.True(t, IsGhostUserName("gHost"))
u, err = GetPossibleUserByID(db.DefaultContext, -2)
require.NoError(t, err)
assert.Equal(t, "gitea-actions", u.Name)
assert.Equal(t, "gitea-actions", u.LowerName)
assert.True(t, u.IsGiteaActions())
assert.True(t, IsGiteaActionsUserName("Gitea-actionS"))
_, err = GetPossibleUserByID(db.DefaultContext, -3)
require.Error(t, err)
}

@ -93,7 +93,7 @@ func Clean(storage ObjectStorage) error {
} }
// SaveFrom saves data to the ObjectStorage with path p from the callback // SaveFrom saves data to the ObjectStorage with path p from the callback
func SaveFrom(objStorage ObjectStorage, p string, callback func(w io.Writer) error) error { func SaveFrom(objStorage ObjectStorage, path string, callback func(w io.Writer) error) error {
pr, pw := io.Pipe() pr, pw := io.Pipe()
defer pr.Close() defer pr.Close()
go func() { go func() {
@ -103,7 +103,7 @@ func SaveFrom(objStorage ObjectStorage, p string, callback func(w io.Writer) err
} }
}() }()
_, err := objStorage.Save(p, pr, -1) _, err := objStorage.Save(path, pr, -1)
return err return err
} }

@ -4,7 +4,6 @@
package user package user
import ( import (
"strings"
"time" "time"
"code.gitea.io/gitea/models/avatars" "code.gitea.io/gitea/models/avatars"
@ -21,32 +20,23 @@ func cacheableRedirect(ctx *context.Context, location string) {
ctx.Redirect(location) ctx.Redirect(location)
} }
// AvatarByUserName redirect browser to user avatar of requested size // AvatarByUsernameSize redirect browser to user avatar of requested size
func AvatarByUserName(ctx *context.Context) { func AvatarByUsernameSize(ctx *context.Context) {
userName := ctx.PathParam(":username") username := ctx.PathParam("username")
size := int(ctx.PathParamInt64(":size")) user := user_model.GetSystemUserByName(username)
if user == nil {
var user *user_model.User
if strings.ToLower(userName) != user_model.GhostUserLowerName {
var err error var err error
if user, err = user_model.GetUserByName(ctx, userName); err != nil { if user, err = user_model.GetUserByName(ctx, username); err != nil {
if user_model.IsErrUserNotExist(err) { ctx.NotFoundOrServerError("GetUserByName", user_model.IsErrUserNotExist, err)
ctx.NotFound("GetUserByName", err)
return
}
ctx.ServerError("Invalid user: "+userName, err)
return return
} }
} else {
user = user_model.NewGhostUser()
} }
cacheableRedirect(ctx, user.AvatarLinkWithSize(ctx, int(ctx.PathParamInt64("size"))))
cacheableRedirect(ctx, user.AvatarLinkWithSize(ctx, size))
} }
// AvatarByEmailHash redirects the browser to the email avatar link // AvatarByEmailHash redirects the browser to the email avatar link
func AvatarByEmailHash(ctx *context.Context) { func AvatarByEmailHash(ctx *context.Context) {
hash := ctx.PathParam(":hash") hash := ctx.PathParam("hash")
email, err := avatars.GetEmailForHash(ctx, hash) email, err := avatars.GetEmailForHash(ctx, hash)
if err != nil { if err != nil {
ctx.ServerError("invalid avatar hash: "+hash, err) ctx.ServerError("invalid avatar hash: "+hash, err)

@ -734,7 +734,7 @@ func UsernameSubRoute(ctx *context.Context) {
switch { switch {
case strings.HasSuffix(username, ".png"): case strings.HasSuffix(username, ".png"):
if reloadParam(".png") { if reloadParam(".png") {
AvatarByUserName(ctx) AvatarByUsernameSize(ctx)
} }
case strings.HasSuffix(username, ".keys"): case strings.HasSuffix(username, ".keys"):
if reloadParam(".keys") { if reloadParam(".keys") {

@ -681,7 +681,7 @@ func registerRoutes(m *web.Router) {
m.Get("/activate", auth.Activate) m.Get("/activate", auth.Activate)
m.Post("/activate", auth.ActivatePost) m.Post("/activate", auth.ActivatePost)
m.Any("/activate_email", auth.ActivateEmail) m.Any("/activate_email", auth.ActivateEmail)
m.Get("/avatar/{username}/{size}", user.AvatarByUserName) m.Get("/avatar/{username}/{size}", user.AvatarByUsernameSize)
m.Get("/recover_account", auth.ResetPasswd) m.Get("/recover_account", auth.ResetPasswd)
m.Post("/recover_account", auth.ResetPasswdPost) m.Post("/recover_account", auth.ResetPasswdPost)
m.Get("/forgot_password", auth.ForgotPasswd) m.Get("/forgot_password", auth.ForgotPasswd)

@ -117,7 +117,7 @@ func (input *notifyInput) Notify(ctx context.Context) {
func notify(ctx context.Context, input *notifyInput) error { func notify(ctx context.Context, input *notifyInput) error {
shouldDetectSchedules := input.Event == webhook_module.HookEventPush && input.Ref.BranchName() == input.Repo.DefaultBranch shouldDetectSchedules := input.Event == webhook_module.HookEventPush && input.Ref.BranchName() == input.Repo.DefaultBranch
if input.Doer.IsActions() { if input.Doer.IsGiteaActions() {
// avoiding triggering cyclically, for example: // avoiding triggering cyclically, for example:
// a comment of an issue will trigger the runner to add a new comment as reply, // a comment of an issue will trigger the runner to add a new comment as reply,
// and the new comment will trigger the runner again. // and the new comment will trigger the runner again.

Loading…
Cancel
Save