chore: form binding trim space (#38978)

Use "binding:TrimSpace" instead of fragile IsEmptyString

And fix a bug in locale's `HasKey`: it should also try the default
language if current language doesn't have the translation key, a new
test is added.
This commit is contained in:
wxiaoguang
2026-08-19 20:50:06 +08:00
committed by GitHub
parent c121f02a7e
commit f261adb53f
17 changed files with 21 additions and 71 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ toolchain go1.26.6
require (
connectrpc.com/connect v1.20.0
gitea.com/go-chi/binding v0.0.0-20260818211407-ac8602c87be9
gitea.com/go-chi/binding v0.0.0-20260819122636-082915a69981
gitea.com/go-chi/cache v0.2.1
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6
+2 -2
View File
@@ -8,8 +8,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
gitea.com/go-chi/binding v0.0.0-20260818211407-ac8602c87be9 h1:J/NzRmGh7olgthuZ096FNdJcQWuEpJb3ycEi6yArlGY=
gitea.com/go-chi/binding v0.0.0-20260818211407-ac8602c87be9/go.mod h1:q1SSPpkC9A0gfNnoqqZ3My6kEHIpKN6QsTI+Zx73B/o=
gitea.com/go-chi/binding v0.0.0-20260819122636-082915a69981 h1:LmdlwGbzgZFZA3bK3R1q8QrNabaSz3KpnOJBLmzhN6E=
gitea.com/go-chi/binding v0.0.0-20260819122636-082915a69981/go.mod h1:q1SSPpkC9A0gfNnoqqZ3My6kEHIpKN6QsTI+Zx73B/o=
gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q=
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
+2 -2
View File
@@ -58,7 +58,7 @@ type CreateProjectOption struct {
// EditProjectOption represents options for editing a project
// swagger:model
type EditProjectOption struct {
Title *string `json:"title,omitempty"`
Title *string `json:"title,omitempty" binding:"TrimSpace;Required"`
Description *string `json:"description,omitempty"`
// Card type: "text_only" or "images_and_text"
CardType *string `json:"card_type,omitempty"`
@@ -94,7 +94,7 @@ type CreateProjectColumnOption struct {
// EditProjectColumnOption represents options for editing a project column
// swagger:model
type EditProjectColumnOption struct {
Title *string `json:"title,omitempty"`
Title *string `json:"title,omitempty" binding:"TrimSpace;Required"`
// Column color in 6-digit hex format, e.g. #FF0000
Color *string `json:"color,omitempty"`
// Position of the column within the project, between -128 and 127
+1 -1
View File
@@ -43,7 +43,7 @@ type WikiPageMetaData struct {
// CreateWikiPageOptions form for creating wiki
type CreateWikiPageOptions struct {
// page title. leave empty to keep unchanged
Title string `json:"title"`
Title string `json:"title" binding:"TrimSpace;Required"`
// content must be base64 encoded
ContentBase64 string `json:"content_base64"`
// optional commit message summarizing the change
+4
View File
@@ -16,6 +16,7 @@ func TestLocaleStore(t *testing.T) {
{
".dot.name": "Dot Name",
"fmt": "%[1]s %[2]s",
"only_in_default": "",
"section.sub": "Sub String",
"section.mixed": "test value; <span style=\"color: red; background: none;\">%s</span>"
@@ -60,6 +61,9 @@ func TestLocaleStore(t *testing.T) {
found := lang1.HasKey("no-such")
assert.False(t, found)
found = lang2.HasKey("only_in_default")
assert.True(t, found)
assert.NoError(t, ls.Close())
res := lang1.TrHTML("<no-such>")
+3
View File
@@ -177,5 +177,8 @@ func (l *locale) HasKey(trKey string) bool {
return false
}
_, ok = l.idxToMsgMap[idx]
if !ok {
_, ok = l.store.localeMap[l.store.defaultLang]
}
return ok
}
-6
View File
@@ -13,7 +13,6 @@ import (
rand2 "math/rand/v2"
"slices"
"strconv"
"strings"
"sync"
"gitea.dev/modules/container"
@@ -22,11 +21,6 @@ import (
"golang.org/x/text/language"
)
// IsEmptyString checks if the provided string is empty
func IsEmptyString(s string) bool {
return len(strings.TrimSpace(s)) == 0
}
// ParseYamlBool parses YAML 1.2 boolean values into bool
func ParseYamlBool(s string) bool {
return s == "true" || s == "True" || s == "TRUE"
-16
View File
@@ -11,22 +11,6 @@ import (
"github.com/stretchr/testify/assert"
)
func TestIsEmptyString(t *testing.T) {
cases := []struct {
s string
expected bool
}{
{"", true},
{" ", true},
{" ", true},
{" a", false},
}
for _, v := range cases {
assert.Equal(t, v.expected, IsEmptyString(v.s))
}
}
func Test_NormalizeEOL(t *testing.T) {
data1 := []string{
"",
+1 -2
View File
@@ -50,7 +50,6 @@ func newFieldError(field reflect.StructField, cls, msg string) *BindingError {
// AddBindingRules adds additional binding rules
func AddBindingRules(b *binding.Binder) {
binding.JSONProvider = jsonProvider{}
b.AddRuleNonZero("GitRefName", func(ctx context.Context, f *binding.ValidationField) *binding.Error {
if !git.IsValidRefPattern(f.ValueMustString()) {
return newFieldError(f.StructField, ErrGitRefName, "GitRefName")
@@ -140,7 +139,7 @@ func validPort(p string) bool {
}
var Binder = sync.OnceValue(func() *binding.Binder {
b := binding.NewBinder().WithDefaultRules().WithNameMapper(util.ToSnakeCase)
b := binding.NewBinder().WithJSONProvider(jsonProvider{}).WithDefaultRules().WithNameMapper(util.ToSnakeCase)
AddBindingRules(b)
return b
})
+1 -1
View File
@@ -532,6 +532,7 @@
"form.CommitMessage": "Commit message",
"form.CommitChoice": "Commit choice",
"form.TreeName": "File path",
"form.Title": "Title",
"form.Content": "Content",
"form.SSPISeparatorReplacement": "Separator",
"form.SSPIDefaultLanguage": "Default Language",
@@ -1418,7 +1419,6 @@
"repo.issues.filter_no_results": "No results",
"repo.issues.filter_no_results_placeholder": "Try adjusting your search filters.",
"repo.issues.new": "New Issue",
"repo.issues.new.title_empty": "Title cannot be empty",
"repo.issues.new.labels": "Labels",
"repo.issues.new.no_labels": "No labels",
"repo.issues.new.clear_labels": "Clear labels",
-7
View File
@@ -13,7 +13,6 @@ import (
"gitea.dev/modules/git"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/convert"
@@ -56,12 +55,6 @@ func NewWikiPage(ctx *context.APIContext) {
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm[*api.CreateWikiPageOptions](ctx)
if util.IsEmptyString(form.Title) {
ctx.APIError(http.StatusBadRequest, "title is required")
return
}
wikiName := wiki_service.UserTitleToWebPath("", form.Title)
if len(form.Message) == 0 {
-8
View File
@@ -612,10 +612,6 @@ func EditProject(ctx *context.APIContext) {
}
form := web.GetForm[*api.EditProjectOption](ctx)
if form.Title != nil && util.IsEmptyString(*form.Title) {
ctx.APIError(http.StatusUnprocessableEntity, "title must not be empty")
return
}
opts := project_service.UpdateProjectOptions{
Title: optional.FromPtr(form.Title),
Description: optional.FromPtr(form.Description),
@@ -1187,10 +1183,6 @@ func EditProjectColumn(ctx *context.APIContext) {
form := web.GetForm[*api.EditProjectColumnOption](ctx)
if form.Title != nil {
if util.IsEmptyString(*form.Title) {
ctx.APIError(http.StatusUnprocessableEntity, "title must not be empty")
return
}
column.Title = *form.Title
}
if form.Color != nil {
+1 -2
View File
@@ -18,7 +18,6 @@ import (
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
auth_service "gitea.dev/services/auth"
"gitea.dev/services/auth/source/ldap"
@@ -210,7 +209,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
}
func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) {
if util.IsEmptyString(form.SSPISeparatorReplacement) {
if form.SSPISeparatorReplacement == "" {
ctx.Data["Err_SSPISeparatorReplacement"] = true
return nil, errors.New(ctx.Locale.TrString("form.require_error", ctx.Locale.TrString("form.SSPISeparatorReplacement")))
}
-5
View File
@@ -351,11 +351,6 @@ func NewIssuePost(ctx *context.Context) {
return
}
if util.IsEmptyString(form.Title) {
ctx.JSONError(ctx.Tr("repo.issues.new.title_empty"))
return
}
content := form.Content
if filename := ctx.Req.Form.Get("template-file"); filename != "" {
if template, err := issue_template.UnmarshalFromRepo(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil {
-5
View File
@@ -1353,11 +1353,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
return
}
if util.IsEmptyString(form.Title) {
ctx.JSONError(ctx.Tr("repo.issues.new.title_empty"))
return
}
// Check if a pull request already exists with the same head and base branch.
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, repo.ID, ci.HeadRef.ShortName(), ci.BaseRef.ShortName(), issues_model.PullRequestFlowGithub)
if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
+1 -1
View File
@@ -87,6 +87,6 @@ type AuthenticationForm struct {
SSPIAutoCreateUsers bool
SSPIAutoActivateUsers bool
SSPIStripDomainNames bool
SSPISeparatorReplacement string `binding:"AlphaDashDot;MaxSize(5)"`
SSPISeparatorReplacement string `binding:"TrimSpace;AlphaDashDot;MaxSize(5)"`
SSPIDefaultLanguage string
}
+4 -12
View File
@@ -336,7 +336,7 @@ type NewPackagistHookForm struct {
// CreateIssueForm form for creating issue
type CreateIssueForm struct {
middleware.FormDefaultValidator
Title string `binding:"Required;MaxSize(255)"`
Title string `binding:"TrimSpace;Required;MaxSize(255)"`
AssigneeIDs string `form:"assignee_ids"`
ReviewerIDs string `form:"reviewer_ids"`
Ref string `form:"ref"`
@@ -550,7 +550,7 @@ type GenerateReleaseNotesForm struct {
// EditReleaseForm form for changing release
type EditReleaseForm struct {
middleware.FormDefaultValidator
Title string `form:"title" binding:"Required;MaxSize(255)"`
Title string `form:"title" binding:"TrimSpace;Required;MaxSize(255)"`
Content string `form:"content"`
Draft string `form:"draft"`
Prerelease bool `form:"prerelease"`
@@ -558,20 +558,12 @@ type EditReleaseForm struct {
}
type WikiEditForm struct {
Title string
middleware.FormDefaultValidator
Title string `binding:"TrimSpace;Required"`
Content string
Message string
}
func (f *WikiEditForm) Validate(ctx *middleware.ValidateContext, errs validation.BindingErrors) validation.BindingErrors {
f.Title = strings.TrimSpace(f.Title)
if f.Title == "" {
errs = middleware.AddValidationError(errs, "title", ctx.Locale.TrString("repo.issues.new.title_empty"))
}
f.Message = strings.TrimSpace(f.Message)
return errs
}
// AddTimeManuallyForm form that adds spent time manually.
type AddTimeManuallyForm struct {
middleware.FormDefaultValidator