mirror of
https://github.com/go-gitea/gitea
synced 2025-02-01 20:44:28 +00:00
Fix user avatar (#33439)
This commit is contained in:
parent
b6fd8741ee
commit
a8eaf43f97
@ -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
|
||||||
|
}
|
||||||
|
32
models/user/user_system_test.go
Normal file
32
models/user/user_system_test.go
Normal file
@ -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…
x
Reference in New Issue
Block a user