From 39341df8cbff2aeb80d3e555c764c621d17548de Mon Sep 17 00:00:00 2001 From: Kerwin Bryant Date: Mon, 31 Mar 2025 11:56:03 +0800 Subject: [PATCH 1/7] Fix markup content overflow (#34072) Fix #34069: use `overflow-wrap: anywhere` to correctly wrap overflowed content. (cherry picked from commit 0fd5392087d35ebe2268f32de422342020e573b9) --- web_src/css/markup/content.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index 947480a7e8..6574d413fc 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -2,7 +2,7 @@ overflow: hidden; font-size: 16px; line-height: 1.5 !important; - word-wrap: break-word; + overflow-wrap: anywhere; } .markup > *:first-child { From 02d9c7cd27e05ef86a981b1a14ce9513e18b6311 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 1 Apr 2025 18:03:27 -0700 Subject: [PATCH 2/7] Return default avatar url when user id is zero rather than updating database (#34094) (cherry picked from commit 88352e0b252e9186a5633d39124a8a65ab89f831) --- models/user/avatar.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/user/avatar.go b/models/user/avatar.go index 27af7f774d..d534bd7bea 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -62,7 +62,7 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error { // 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 { - if u.IsGhost() { + if u.IsGhost() || u.ID <= 0 { return avatars.DefaultAvatarLink() } From f7b19964a7fff9a5d6be01a7f14bdd1c1f3c8f41 Mon Sep 17 00:00:00 2001 From: Kemal Zebari <60799661+kemzeb@users.noreply.github.com> Date: Wed, 2 Apr 2025 07:00:54 -0700 Subject: [PATCH 3/7] Add new CLI flags to set name and scopes when creating a user with access token (#34080) Resolves #33474. --------- Co-authored-by: wxiaoguang (cherry picked from commit 55a69ae4c63ee8551eadb161cb901ba0a2a2e194) --- cmd/admin_user_create.go | 46 +++++++++++++++++++----- cmd/admin_user_generate_access_token.go | 9 +++-- models/auth/access_token_scope.go | 4 +++ routers/web/user/setting/applications.go | 3 ++ 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index 33e61c0753..81a2a828de 100644 --- a/cmd/admin_user_create.go +++ b/cmd/admin_user_create.go @@ -6,6 +6,7 @@ package cmd import ( "errors" "fmt" + "strings" auth_model "forgejo.org/models/auth" "forgejo.org/models/db" @@ -61,6 +62,16 @@ var microcmdUserCreate = &cli.Command{ Name: "access-token", Usage: "Generate access token for the user", }, + &cli.StringFlag{ + Name: "access-token-name", + Usage: `Name of the generated access token`, + Value: "gitea-admin", + }, + &cli.StringFlag{ + Name: "access-token-scopes", + Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`, + Value: "all", + }, &cli.BoolFlag{ Name: "restricted", Usage: "Make a restricted user account", @@ -157,23 +168,40 @@ func runCreateUser(c *cli.Context) error { IsRestricted: restricted, } + var accessTokenName string + var accessTokenScope auth_model.AccessTokenScope + if c.IsSet("access-token") { + accessTokenName = strings.TrimSpace(c.String("access-token-name")) + if accessTokenName == "" { + return errors.New("access-token-name cannot be empty") + } + var err error + accessTokenScope, err = auth_model.AccessTokenScope(c.String("access-token-scopes")).Normalize() + if err != nil { + return fmt.Errorf("invalid access token scope provided: %w", err) + } + if !accessTokenScope.HasPermissionScope() { + return errors.New("access token does not have any permission") + } + } else if c.IsSet("access-token-name") || c.IsSet("access-token-scopes") { + return errors.New("access-token-name and access-token-scopes flags are only valid when access-token flag is set") + } + + // arguments should be prepared before creating the user & access token, in case there is anything wrong + + // create the user if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil { return fmt.Errorf("CreateUser: %w", err) } + fmt.Printf("New user '%s' has been successfully created!\n", username) - if c.Bool("access-token") { - t := &auth_model.AccessToken{ - Name: "gitea-admin", - UID: u.ID, - } - + // create the access token + if accessTokenScope != "" { + t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope} if err := auth_model.NewAccessToken(ctx, t); err != nil { return err } - fmt.Printf("Access token was successfully created... %s\n", t.Token) } - - fmt.Printf("New user '%s' has been successfully created!\n", username) return nil } diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go index abb874bd5f..1a6c003171 100644 --- a/cmd/admin_user_generate_access_token.go +++ b/cmd/admin_user_generate_access_token.go @@ -34,8 +34,8 @@ var microcmdUserGenerateAccessToken = &cli.Command{ }, &cli.StringFlag{ Name: "scopes", - Value: "", - Usage: "Comma separated list of scopes to apply to access token", + Value: "all", + Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`, }, }, Action: runGenerateAccessToken, @@ -43,7 +43,7 @@ var microcmdUserGenerateAccessToken = &cli.Command{ func runGenerateAccessToken(c *cli.Context) error { if !c.IsSet("username") { - return errors.New("You must provide a username to generate a token for") + return errors.New("you must provide a username to generate a token for") } ctx, cancel := installSignals() @@ -77,6 +77,9 @@ func runGenerateAccessToken(c *cli.Context) error { if err != nil { return fmt.Errorf("invalid access token scope provided: %w", err) } + if !accessTokenScope.HasPermissionScope() { + return errors.New("access token does not have any permission") + } t.Scope = accessTokenScope // create the token diff --git a/models/auth/access_token_scope.go b/models/auth/access_token_scope.go index 802ad5aa07..d14838cf02 100644 --- a/models/auth/access_token_scope.go +++ b/models/auth/access_token_scope.go @@ -283,6 +283,10 @@ func (s AccessTokenScope) Normalize() (AccessTokenScope, error) { return bitmap.toScope(), nil } +func (s AccessTokenScope) HasPermissionScope() bool { + return s != "" && s != AccessTokenScopePublicOnly +} + // PublicOnly checks if this token scope is limited to public resources func (s AccessTokenScope) PublicOnly() (bool, error) { bitmap, err := s.parse() diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go index 631d5958ea..e73239b79b 100644 --- a/routers/web/user/setting/applications.go +++ b/routers/web/user/setting/applications.go @@ -49,6 +49,9 @@ func ApplicationsPost(ctx *context.Context) { ctx.ServerError("GetScope", err) return } + if !scope.HasPermissionScope() { + ctx.Flash.Error(ctx.Tr("settings.at_least_one_permission"), true) + } t := &auth_model.AccessToken{ UID: ctx.Doer.ID, Name: form.Name, From 51990751a696a2b87af0a4373093f646312ec82f Mon Sep 17 00:00:00 2001 From: ManInDark <61268856+ManInDark@users.noreply.github.com> Date: Thu, 3 Apr 2025 20:34:54 +0200 Subject: [PATCH 4/7] also check default ssh-cert location for host (#34099) (#34100) (cherry picked from commit f8d549436ea70142514557b6a1d23d578d6aef42) --- docker/root/etc/s6/openssh/setup | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docker/root/etc/s6/openssh/setup b/docker/root/etc/s6/openssh/setup index dbb3bafd35..6fbc599cc5 100755 --- a/docker/root/etc/s6/openssh/setup +++ b/docker/root/etc/s6/openssh/setup @@ -31,6 +31,18 @@ if [ -e /data/ssh/ssh_host_ecdsa_cert ]; then SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_cert"} fi +if [ -e /data/ssh/ssh_host_ed25519-cert.pub ]; then + SSH_ED25519_CERT=${SSH_ED25519_CERT:-"/data/ssh/ssh_host_ed25519-cert.pub"} +fi + +if [ -e /data/ssh/ssh_host_rsa-cert.pub ]; then + SSH_RSA_CERT=${SSH_RSA_CERT:-"/data/ssh/ssh_host_rsa-cert.pub"} +fi + +if [ -e /data/ssh/ssh_host_ecdsa-cert.pub ]; then + SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa-cert.pub"} +fi + if [ -d /etc/ssh ]; then SSH_PORT=${SSH_PORT:-"22"} \ SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-"${SSH_PORT}"} \ From 3e3a109dd23f263a498032fa0db811dac8f65c07 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Thu, 3 Apr 2025 21:00:00 +0200 Subject: [PATCH 5/7] Fix invalid version in RPM package path (#34112) (cherry picked from commit 8fed70afdc0f63636cdd6f5ea3fdf88061ad8dc2) --- services/packages/rpm/repository.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/packages/rpm/repository.go b/services/packages/rpm/repository.go index 961de7828f..f3b6f5148a 100644 --- a/services/packages/rpm/repository.go +++ b/services/packages/rpm/repository.go @@ -410,7 +410,6 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs [] files = append(files, f) } } - packageVersion := fmt.Sprintf("%s-%s", pd.FileMetadata.Version, pd.FileMetadata.Release) packages = append(packages, &Package{ Type: "rpm", Name: pd.Package.Name, @@ -439,7 +438,7 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs [] Archive: pd.FileMetadata.ArchiveSize, }, Location: Location{ - Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture, pd.Package.Name, packageVersion, pd.FileMetadata.Architecture), + Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture, pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture), }, Format: Format{ License: pd.VersionMetadata.License, From 64cc883ac1fdd22b8c70aac963b2a381486385f2 Mon Sep 17 00:00:00 2001 From: Mopcho <47301161+Mopcho@users.noreply.github.com> Date: Fri, 4 Apr 2025 21:09:40 +0300 Subject: [PATCH 6/7] Fix discord webhook 400 status code when description limit is exceeded (#34084) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes [#34027](https://github.com/go-gitea/gitea/issues/34027) Discord does not allow for description bigger than 2048 bytes. If the description is bigger than that it will throw 400 and the event won't appear in discord. To fix that, in the createPayload method we now slice the description to ensure it doesn’t exceed the limit. --------- Co-authored-by: wxiaoguang (cherry picked from commit 013b2686fe6d306c4fb800147207b099866683b9) --- modules/util/truncate.go | 9 +++++++++ modules/util/truncate_test.go | 15 +++++++++++++++ services/webhook/discord.go | 19 +++++++++++++------ services/webhook/discord_test.go | 2 +- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/modules/util/truncate.go b/modules/util/truncate.go index f2edbdc673..7207a89177 100644 --- a/modules/util/truncate.go +++ b/modules/util/truncate.go @@ -54,3 +54,12 @@ func SplitTrimSpace(input, sep string) []string { return stringList } + +// TruncateRunes returns a truncated string with given rune limit, +// it returns input string if its rune length doesn't exceed the limit. +func TruncateRunes(str string, limit int) string { + if utf8.RuneCountInString(str) < limit { + return str + } + return string([]rune(str)[:limit]) +} diff --git a/modules/util/truncate_test.go b/modules/util/truncate_test.go index dfe1230fd4..8187b13eb2 100644 --- a/modules/util/truncate_test.go +++ b/modules/util/truncate_test.go @@ -44,3 +44,18 @@ func TestSplitString(t *testing.T) { } test(tc, SplitStringAtByteN) } + +func TestTruncateRunes(t *testing.T) { + assert.Empty(t, TruncateRunes("", 0)) + assert.Empty(t, TruncateRunes("", 1)) + + assert.Empty(t, TruncateRunes("ab", 0)) + assert.Equal(t, "a", TruncateRunes("ab", 1)) + assert.Equal(t, "ab", TruncateRunes("ab", 2)) + assert.Equal(t, "ab", TruncateRunes("ab", 3)) + + assert.Empty(t, TruncateRunes("测试", 0)) + assert.Equal(t, "测", TruncateRunes("测试", 1)) + assert.Equal(t, "测试", TruncateRunes("测试", 2)) + assert.Equal(t, "测试", TruncateRunes("测试", 3)) +} diff --git a/services/webhook/discord.go b/services/webhook/discord.go index 3970a2552d..f60903c276 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -151,6 +151,18 @@ var ( redColor = color("ff3232") ) +// https://discord.com/developers/docs/resources/message#embed-object-embed-limits +// Discord has some limits in place for the embeds. +// According to some tests, there is no consistent limit for different character sets. +// For example: 4096 ASCII letters are allowed, but only 2490 emoji characters are allowed. +// To keep it simple, we currently truncate at 2000. +const discordDescriptionCharactersLimit = 2000 + +type discordConvertor struct { + Username string + AvatarURL string +} + // Create implements PayloadConvertor Create method func (d discordConvertor) Create(p *api.CreatePayload) (DiscordPayload, error) { // created tag/branch @@ -312,11 +324,6 @@ func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error) return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil } -type discordConvertor struct { - Username string - AvatarURL string -} - var _ shared.PayloadConvertor[DiscordPayload] = discordConvertor{} func (discordHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { @@ -357,7 +364,7 @@ func (d discordConvertor) createPayload(s *api.User, title, text, url string, co Embeds: []DiscordEmbed{ { Title: title, - Description: text, + Description: util.TruncateRunes(text, discordDescriptionCharactersLimit), URL: url, Color: color, Author: DiscordEmbedAuthor{ diff --git a/services/webhook/discord_test.go b/services/webhook/discord_test.go index ce3aaa10cf..b04be30bc6 100644 --- a/services/webhook/discord_test.go +++ b/services/webhook/discord_test.go @@ -175,7 +175,7 @@ func TestDiscordPayload(t *testing.T) { require.NoError(t, err) assert.Len(t, pl.Embeds, 1) - assert.Len(t, pl.Embeds[0].Description, 4096) + assert.Len(t, pl.Embeds[0].Description, 2000) }) t.Run("IssueComment", func(t *testing.T) { From 2a1759ba3be4fc63bf79d40e17ca86ae109c9e4e Mon Sep 17 00:00:00 2001 From: Michael Jerger Date: Thu, 10 Apr 2025 13:49:18 +0200 Subject: [PATCH 7/7] use base.TruncateString instead of TruncateRune --- modules/util/truncate.go | 9 --------- modules/util/truncate_test.go | 15 --------------- services/webhook/discord.go | 3 ++- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/modules/util/truncate.go b/modules/util/truncate.go index 7207a89177..f2edbdc673 100644 --- a/modules/util/truncate.go +++ b/modules/util/truncate.go @@ -54,12 +54,3 @@ func SplitTrimSpace(input, sep string) []string { return stringList } - -// TruncateRunes returns a truncated string with given rune limit, -// it returns input string if its rune length doesn't exceed the limit. -func TruncateRunes(str string, limit int) string { - if utf8.RuneCountInString(str) < limit { - return str - } - return string([]rune(str)[:limit]) -} diff --git a/modules/util/truncate_test.go b/modules/util/truncate_test.go index 8187b13eb2..dfe1230fd4 100644 --- a/modules/util/truncate_test.go +++ b/modules/util/truncate_test.go @@ -44,18 +44,3 @@ func TestSplitString(t *testing.T) { } test(tc, SplitStringAtByteN) } - -func TestTruncateRunes(t *testing.T) { - assert.Empty(t, TruncateRunes("", 0)) - assert.Empty(t, TruncateRunes("", 1)) - - assert.Empty(t, TruncateRunes("ab", 0)) - assert.Equal(t, "a", TruncateRunes("ab", 1)) - assert.Equal(t, "ab", TruncateRunes("ab", 2)) - assert.Equal(t, "ab", TruncateRunes("ab", 3)) - - assert.Empty(t, TruncateRunes("测试", 0)) - assert.Equal(t, "测", TruncateRunes("测试", 1)) - assert.Equal(t, "测试", TruncateRunes("测试", 2)) - assert.Equal(t, "测试", TruncateRunes("测试", 3)) -} diff --git a/services/webhook/discord.go b/services/webhook/discord.go index f60903c276..2b58bf892d 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -16,6 +16,7 @@ import ( "unicode/utf8" webhook_model "forgejo.org/models/webhook" + "forgejo.org/modules/base" "forgejo.org/modules/git" "forgejo.org/modules/json" "forgejo.org/modules/log" @@ -364,7 +365,7 @@ func (d discordConvertor) createPayload(s *api.User, title, text, url string, co Embeds: []DiscordEmbed{ { Title: title, - Description: util.TruncateRunes(text, discordDescriptionCharactersLimit), + Description: base.TruncateString(text, discordDescriptionCharactersLimit), URL: url, Color: color, Author: DiscordEmbedAuthor{