refactor: form binding validation (#38832)

Make "form-fetch-action" highlight the invalid field as "error"
This commit is contained in:
wxiaoguang
2026-08-10 09:25:31 +08:00
committed by GitHub
parent b5aa8c4ff0
commit 9c915e4e1a
27 changed files with 411 additions and 323 deletions
+7 -12
View File
@@ -18,18 +18,13 @@ import (
)
const (
// ErrGitRefName is git reference name error
ErrGitRefName = "GitRefNameError"
// ErrGlobPattern is returned when glob pattern is invalid
ErrGlobPattern = "GlobPattern"
// ErrRegexPattern is returned when a regex pattern is invalid
ErrRegexPattern = "RegexPattern"
// ErrUsername is username error
ErrUsername = "UsernameError"
// ErrInvalidGroupTeamMap is returned when a group team mapping is invalid
ErrCustomMessage = "CustomMessage"
ErrGitRefName = "GitRefNameError"
ErrGlobPattern = "GlobPattern"
ErrRegexPattern = "RegexPattern"
ErrUsername = "UsernameError"
ErrInvalidGroupTeamMap = "InvalidGroupTeamMap"
// ErrInvalidBadgeSlug is returned when a badge slug is invalid
ErrInvalidBadgeSlug = "InvalidBadgeSlug"
ErrInvalidBadgeSlug = "InvalidBadgeSlug"
)
type jsonProvider struct{}
@@ -61,7 +56,7 @@ func AddBindingRules() {
}
func addGitRefNameBindingRule() {
// Git refname validation rule
// Git ref name validation rule
binding.AddRule(&binding.Rule{
IsMatch: func(rule string) bool {
return rule == "GitRefName"
+125 -96
View File
@@ -5,9 +5,12 @@
package middleware
import (
"net/http"
"reflect"
"strings"
"gitea.dev/modules/reqctx"
"gitea.dev/modules/setting"
"gitea.dev/modules/translation"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
@@ -15,6 +18,14 @@ import (
"gitea.com/go-chi/binding"
)
// ValidateContext is a special context for form validation middleware. It may be different from other contexts.
type ValidateContext struct {
Locale translation.Locale
Data reqctx.ContextData
Req *http.Request
Resp http.ResponseWriter
}
// Form form binding interface
type Form interface {
binding.Validator
@@ -49,7 +60,8 @@ func AssignForm(form any, data map[string]any) {
}
}
func getRuleBody(field reflect.StructField, prefix string) string {
func getRuleBody(field reflect.StructField, ruleName string) string {
prefix := ruleName + "("
for rule := range strings.SplitSeq(field.Tag.Get("binding"), ";") {
if strings.HasPrefix(rule, prefix) {
return rule[len(prefix) : len(rule)-1]
@@ -58,117 +70,134 @@ func getRuleBody(field reflect.StructField, prefix string) string {
return ""
}
// GetSize get size int form tag
func GetSize(field reflect.StructField) string {
return getRuleBody(field, "Size(")
}
// GetMinSize get minimal size in form tag
func GetMinSize(field reflect.StructField) string {
return getRuleBody(field, "MinSize(")
}
// GetMaxSize get max size in form tag
func GetMaxSize(field reflect.StructField) string {
return getRuleBody(field, "MaxSize(")
}
// GetInclude get include in form tag
func GetInclude(field reflect.StructField) string {
return getRuleBody(field, "Include(")
}
func ReportValidationError(errs binding.Errors, data map[string]any, fieldName, classification, errorMsg string) binding.Errors {
errs.Add([]string{fieldName}, classification, errorMsg)
data["HasError"] = true
data["ErrorMsg"] = fieldName + ": " + errorMsg
data["Err_"+fieldName] = true
// there is already a reported validation error, so no need to generate default error messages in Validate()
data["HasErrorFormValidation"] = true
func AddValidationError(errs binding.Errors, fieldName, errorMsg string) binding.Errors {
errs.Add([]string{fieldName}, validation.ErrCustomMessage, errorMsg)
return errs
}
func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Locale) binding.Errors {
// try to restore the form's values as much as possible,
// especially for RenderWithErrDeprecated to re-render the form with errors
AssignForm(f, data)
if errs.Len() == 0 || data["HasErrorFormValidation"] == true {
return errs
func getFieldDisplayNameForMessage(f Form, l translation.Locale, fieldNames []string) (field reflect.StructField, ok bool, displayName string) {
if len(fieldNames) == 0 {
return field, false, ""
}
// if HasError=true, then must set default error message
// because still a lot of places use `ctx.Data["ErrorMsg"].(string)` even if the error fields can't be found
data["HasError"] = true
data["ErrorMsg"] = l.TrString("form.unknown_error")
typ := reflect.TypeOf(f)
if typ.Kind() == reflect.Pointer {
typ = typ.Elem()
}
field, fieldExists := typ.FieldByName(errs[0].FieldNames[0])
field, fieldExists := typ.FieldByName(fieldNames[0])
if !fieldExists {
return errs
return field, false, ""
}
if field.Tag.Get("form") == "-" {
return field, false, ""
}
trKeyFallback := "form." + field.Name
trKey := util.IfZero(field.Tag.Get("locale"), trKeyFallback)
displayName = l.TrString(trKey)
if displayName == trKeyFallback {
displayName = field.Name
}
return field, true, displayName
}
func BuildValidationErrorForUser(f Form, l translation.Locale, bindingErrs binding.Errors) (errorMessage, errorFieldName string, fieldNames []string) {
if bindingErrs.Len() == 0 {
return "", "", nil
}
bindingErr := bindingErrs[0]
fieldNames, classification, bindingErrMsg := bindingErr.FieldNames, bindingErr.Classification, bindingErr.Message
field, ok, fieldDisplayName := getFieldDisplayNameForMessage(f, l, fieldNames)
if !ok {
return l.TrString("error.occurred"), "", fieldNames
}
errorFieldName = field.Name
switch classification {
case binding.ERR_REQUIRED:
errorMessage = l.TrString("form.require_error", fieldDisplayName)
case binding.ERR_ALPHA_DASH:
errorMessage = l.TrString("form.alpha_dash_error", fieldDisplayName)
case binding.ERR_ALPHA_DASH_DOT:
errorMessage = l.TrString("form.alpha_dash_dot_error", fieldDisplayName)
case binding.ERR_MIN_SIZE:
errorMessage = l.TrString("form.min_size_error", fieldDisplayName, getRuleBody(field, "MinSize"))
case binding.ERR_MAX_SIZE:
errorMessage = l.TrString("form.max_size_error", fieldDisplayName, getRuleBody(field, "MaxSize"))
case binding.ERR_RANGE:
rangeMin, rangeMax, _ := strings.Cut(getRuleBody(field, "Range"), ",")
errorMessage = l.TrString("form.range_error", fieldDisplayName, rangeMin, rangeMax)
case binding.ERR_EMAIL:
errorMessage = l.TrString("form.email_error", fieldDisplayName)
case binding.ERR_URL:
errorMessage = l.TrString("form.url_error", fieldDisplayName)
case binding.ERR_IN:
ruleBody := getRuleBody(field, "In")
if strings.HasPrefix(ruleBody, ",") {
ruleBody = "(empty)" + ruleBody
}
errorMessage = l.TrString("form.in_error", fieldDisplayName, ruleBody)
case binding.ERR_INCLUDE:
errorMessage = l.TrString("form.include_error", fieldDisplayName, getRuleBody(field, "Include"))
case validation.ErrCustomMessage:
errorMessage = bindingErrMsg
case validation.ErrGitRefName:
errorMessage = l.TrString("form.git_ref_name_error", fieldDisplayName)
case validation.ErrGlobPattern:
errorMessage = l.TrString("form.glob_pattern_error", fieldDisplayName, bindingErrMsg)
case validation.ErrRegexPattern:
errorMessage = l.TrString("form.regex_pattern_error", fieldDisplayName, bindingErrMsg)
case validation.ErrUsername:
errorMessage = l.TrString("form.username_error", fieldDisplayName)
case validation.ErrInvalidGroupTeamMap:
errorMessage = l.TrString("form.invalid_group_team_map_error", fieldDisplayName, bindingErrMsg)
case validation.ErrInvalidBadgeSlug:
errorMessage = l.TrString("form.invalid_slug_error", fieldDisplayName)
default:
setting.PanicInDevOrTesting("unknown binding error classification: %v", classification)
var msg string
if classification != "" && bindingErrMsg != "" {
msg = classification + ": " + bindingErrMsg
} else {
msg = util.IfZero(bindingErrMsg, classification)
if msg == "" {
setting.PanicInDevOrTesting("no error message for binding error: %v", bindingErr)
}
msg = util.IfZero(msg, "unknown error")
}
errorMessage = l.TrString("form.field_invalid_message", fieldDisplayName, msg)
}
return errorMessage, errorFieldName, fieldNames
}
type contextKeySkipTmplFormValidationErrorType struct{}
var contextKeySkipTmplFormValidationError contextKeySkipTmplFormValidationErrorType
func SkipTmplFormValidationError(ctx reqctx.RequestContext) {
ctx.SetContextValue(contextKeySkipTmplFormValidationError, true)
}
func Validate(ctx *ValidateContext, errs binding.Errors, f Form) binding.Errors {
if ctx.Req.Context().Value(contextKeySkipTmplFormValidationError) == true {
// if it is not using tmpl-based validation error handling, just return the errors
// for example: when using "form-fetch-action", the validation error can be handled by GetFetchActionForm
return errs
}
errorMessage, errorFieldName, _ := BuildValidationErrorForUser(f, ctx.Locale, errs)
if errorMessage == "" {
return errs
}
data["Err_"+field.Name] = true
trName := field.Tag.Get("locale")
if len(trName) == 0 {
trName = l.TrString("form." + field.Name)
} else {
trName = l.TrString(trName)
// Legacy template error handling: try to restore the form's values as much as possible,
// especially for RenderWithErrDeprecated to re-render the form with errors.
AssignForm(f, ctx.Data)
ctx.Data["HasError"] = true
ctx.Data["ErrorMsg"] = errorMessage
if errorFieldName != "" {
ctx.Data["Err_"+errorFieldName] = true
}
switch errs[0].Classification {
case binding.ERR_REQUIRED:
data["ErrorMsg"] = trName + l.TrString("form.require_error")
case binding.ERR_ALPHA_DASH:
data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_error")
case binding.ERR_ALPHA_DASH_DOT:
data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_dot_error")
case validation.ErrGitRefName:
data["ErrorMsg"] = trName + l.TrString("form.git_ref_name_error")
case binding.ERR_SIZE:
data["ErrorMsg"] = trName + l.TrString("form.size_error", GetSize(field))
case binding.ERR_MIN_SIZE:
data["ErrorMsg"] = trName + l.TrString("form.min_size_error", GetMinSize(field))
case binding.ERR_MAX_SIZE:
data["ErrorMsg"] = trName + l.TrString("form.max_size_error", GetMaxSize(field))
case binding.ERR_EMAIL:
data["ErrorMsg"] = trName + l.TrString("form.email_error")
case binding.ERR_URL:
data["ErrorMsg"] = trName + l.TrString("form.url_error", errs[0].Message)
case binding.ERR_INCLUDE:
data["ErrorMsg"] = trName + l.TrString("form.include_error", GetInclude(field))
case validation.ErrGlobPattern:
data["ErrorMsg"] = trName + l.TrString("form.glob_pattern_error", errs[0].Message)
case validation.ErrRegexPattern:
data["ErrorMsg"] = trName + l.TrString("form.regex_pattern_error", errs[0].Message)
case validation.ErrUsername:
data["ErrorMsg"] = trName + l.TrString("form.username_error")
case validation.ErrInvalidGroupTeamMap:
data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
case validation.ErrInvalidBadgeSlug:
data["ErrorMsg"] = trName + l.TrString("form.invalid_slug_error")
default:
msg := errs[0].Classification
if msg != "" && errs[0].Message != "" {
msg += ": "
}
msg += errs[0].Message
if msg == "" {
msg = l.TrString("form.unknown_error")
}
data["ErrorMsg"] = trName + ": " + msg
}
return errs
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"gitea.dev/modules/translation"
"gitea.com/go-chi/binding"
"github.com/stretchr/testify/assert"
)
type testRangeForm struct {
Hours int `binding:"Range(0,1000)"`
}
func (f *testRangeForm) Validate(_ *http.Request, errs binding.Errors) binding.Errors { return errs }
func TestBuildValidationErrorForUser(t *testing.T) {
// an out-of-range value must reach its own message instead of the panicking "default" branch
form := &testRangeForm{Hours: 2000}
errs := binding.Validate(httptest.NewRequest(http.MethodPost, "/", nil), form)
errorMessage, errorFieldName, fieldNames := BuildValidationErrorForUser(form, translation.MockLocale{}, errs)
assert.Equal(t, "form.range_error:Hours,0,1000", errorMessage)
assert.Equal(t, "Hours", errorFieldName)
assert.Equal(t, []string{"Hours"}, fieldNames)
}
+5 -1
View File
@@ -26,7 +26,7 @@ func Bind[T any](_ T) http.HandlerFunc {
return func(resp http.ResponseWriter, req *http.Request) {
theObj := new(T) // create a new form obj for every request but not use obj directly
data := middleware.GetContextData(req.Context())
binding.Bind(req, theObj)
_ = binding.Bind(req, theObj) // no need to handle "errs" here, the errors are handled in our middleware.Validate (binding.go)
SetForm(data, theObj)
middleware.AssignForm(theObj, data)
}
@@ -37,6 +37,10 @@ func SetForm(dataStore reqctx.ContextDataProvider, obj any) {
dataStore.GetData()["__form"] = obj
}
func IsFormSet(dataStore reqctx.RequestDataStore) bool {
return dataStore.GetData()["__form"] != nil
}
// GetForm returns the validate form information
func GetForm[T any](dataStore reqctx.RequestDataStore) T {
form, ok := dataStore.GetData()["__form"].(T)
+18 -19
View File
@@ -523,9 +523,6 @@
"form.RepoName": "Repository name",
"form.Email": "Email address",
"form.Password": "Password",
"form.Retype": "Confirm Password",
"form.SSHTitle": "SSH key name",
"form.HttpsUrl": "HTTPS URL",
"form.PayloadUrl": "Payload URL",
"form.TeamName": "Team name",
"form.AuthName": "Authorization name",
@@ -538,22 +535,24 @@
"form.Content": "Content",
"form.SSPISeparatorReplacement": "Separator",
"form.SSPIDefaultLanguage": "Default Language",
"form.require_error": " cannot be empty.",
"form.alpha_dash_error": " should contain only alphanumeric, dash ('-') and underscore ('_') characters.",
"form.alpha_dash_dot_error": " should contain only alphanumeric, dash ('-'), underscore ('_') and dot ('.') characters.",
"form.git_ref_name_error": " must be a well-formed Git reference name.",
"form.size_error": " must be size %s.",
"form.min_size_error": " must contain at least %s characters.",
"form.max_size_error": " must contain at most %s characters.",
"form.email_error": " is not a valid email address.",
"form.url_error": "\"%s\" is not a valid URL.",
"form.include_error": " must contain substring \"%s\".",
"form.glob_pattern_error": " glob pattern is invalid: %s.",
"form.regex_pattern_error": " regex pattern is invalid: %s.",
"form.username_error": " can only contain alphanumeric characters ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric characters, and consecutive non-alphanumeric characters are also forbidden.",
"form.invalid_slug_error": " is invalid.",
"form.invalid_group_team_map_error": " mapping is invalid: %s",
"form.unknown_error": "Unknown error:",
"form.require_error": "%s cannot be empty.",
"form.alpha_dash_error": "%s should contain only alphanumeric, dash ('-') and underscore ('_') characters.",
"form.alpha_dash_dot_error": "%s should contain only alphanumeric, dash ('-'), underscore ('_') and dot ('.') characters.",
"form.git_ref_name_error": "%s must be a well-formed Git reference name.",
"form.size_error": "%[1]s must be size %[2]s.",
"form.min_size_error": "%[1]s must contain at least %[2]s characters.",
"form.max_size_error": "%[1]s must contain at most %[2]s characters.",
"form.range_error": "%[1]s must be a number from %[2]s to %[3]s.",
"form.email_error": "%s is not a valid email address.",
"form.url_error": "%s is not a valid URL.",
"form.in_error": "%[1]s must be one of the following: %[2]s.",
"form.include_error": "%[1]s must contain substring \"%[2]s\".",
"form.glob_pattern_error": "%[1]s glob pattern is invalid: %[2]s.",
"form.regex_pattern_error": "%[1]s regex pattern is invalid: %[2]s.",
"form.username_error": "%s can only contain alphanumeric characters ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric characters, and consecutive non-alphanumeric characters are also forbidden.",
"form.invalid_slug_error": "%s is invalid.",
"form.invalid_group_team_map_error": "%[1]s mapping is invalid: %[2]s",
"form.field_invalid_message": "%[1]s is invalid: %[2]s",
"form.captcha_incorrect": "The CAPTCHA code is incorrect.",
"form.password_not_match": "The passwords do not match.",
"form.lang_select_error": "Select a language from the list.",
+2 -2
View File
@@ -212,11 +212,11 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) {
if util.IsEmptyString(form.SSPISeparatorReplacement) {
ctx.Data["Err_SSPISeparatorReplacement"] = true
return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.require_error"))
return nil, errors.New(ctx.Locale.TrString("form.require_error", ctx.Locale.TrString("form.SSPISeparatorReplacement")))
}
if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
ctx.Data["Err_SSPISeparatorReplacement"] = true
return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.alpha_dash_dot_error"))
return nil, errors.New(ctx.Locale.TrString("form.alpha_dash_dot_error", ctx.Locale.TrString("form.SSPISeparatorReplacement")))
}
if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) {
+14 -39
View File
@@ -4,13 +4,12 @@
package setting
import (
"errors"
"net/http"
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/models/db"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/web"
asymkey_service "gitea.dev/services/asymkey"
"gitea.dev/services/context"
"gitea.dev/services/forms"
@@ -32,69 +31,45 @@ func DeployKeys(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplDeployKeys)
}
// DeployKeysPost response for adding a deploy key of a repository
// DeployKeysPost response for adding a deploy-key of a repository
func DeployKeysPost(ctx *context.Context) {
form := web.GetForm[*forms.AddKeyForm](ctx)
ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
ctx.Data["PageIsSettingsKeys"] = true
ctx.Data["DisableSSH"] = setting.SSH.Disabled
keys, err := db.Find[asymkey_model.DeployKey](ctx, asymkey_model.ListDeployKeysOptions{RepoID: ctx.Repo.Repository.ID})
if err != nil {
ctx.ServerError("ListDeployKeys", err)
form := context.GetFetchActionForm[*forms.AddKeyForm](ctx)
if form == nil {
return
}
ctx.Data["Deploykeys"] = keys
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplDeployKeys)
return
}
content, err := asymkey_model.CheckPublicKeyString(form.Content)
if err != nil {
if db.IsErrSSHDisabled(err) {
ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
ctx.JSONError(ctx.Tr("settings.ssh_disabled"))
} else if asymkey_model.IsErrKeyUnableVerify(err) {
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
} else if err == asymkey_model.ErrKeyIsPrivate {
ctx.Data["HasError"] = true
ctx.Data["Err_Content"] = true
ctx.Flash.Error(ctx.Tr("form.must_use_public_key"))
ctx.JSONErrorWithField(ctx.Tr("form.unable_verify_ssh_key"), "content")
} else if errors.Is(err, asymkey_model.ErrKeyIsPrivate) {
ctx.JSONErrorWithField(ctx.Tr("form.must_use_public_key"), "content")
} else {
ctx.Data["HasError"] = true
ctx.Data["Err_Content"] = true
ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
ctx.JSONErrorWithField(ctx.Tr("form.invalid_ssh_key", err.Error()), "content")
}
ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
return
}
key, err := asymkey_model.AddDeployKey(ctx, ctx.Repo.Repository.ID, form.Title, content, !form.IsWritable)
if err != nil {
ctx.Data["HasError"] = true
switch {
case asymkey_model.IsErrDeployKeyAlreadyExist(err):
ctx.Data["Err_Content"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form)
ctx.JSONErrorWithField(ctx.Tr("repo.settings.key_been_used"), "content")
case asymkey_model.IsErrKeyAlreadyExist(err):
ctx.Data["Err_Content"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form)
ctx.JSONErrorWithField(ctx.Tr("settings.ssh_key_been_used"), "content")
case asymkey_model.IsErrKeyNameAlreadyUsed(err):
ctx.Data["Err_Title"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
ctx.JSONErrorWithField(ctx.Tr("repo.settings.key_name_used"), "title")
case asymkey_model.IsErrDeployKeyNameAlreadyUsed(err):
ctx.Data["Err_Title"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
ctx.JSONErrorWithField(ctx.Tr("repo.settings.key_name_used"), "title")
default:
ctx.ServerError("AddDeployKey", err)
}
return
}
log.Trace("Deploy key added: %d", ctx.Repo.Repository.ID)
ctx.Flash.Success(ctx.Tr("repo.settings.add_key_success", key.Name))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/keys")
}
// DeleteDeployKey response for deleting a deploy key
+18 -45
View File
@@ -5,6 +5,7 @@ package setting
import (
"net/http"
"net/url"
"testing"
asymkey_model "gitea.dev/models/asymkey"
@@ -27,53 +28,25 @@ import (
"github.com/stretchr/testify/require"
)
func TestAddReadOnlyDeployKey(t *testing.T) {
defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
func TestAddDeployKey(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "user2/repo1/settings/keys")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 2)
addKeyForm := forms.AddKeyForm{
Title: "read-only",
Content: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
}
web.SetForm(ctx, &addKeyForm)
DeployKeysPost(ctx)
assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
Name: addKeyForm.Title,
Content: addKeyForm.Content,
Mode: perm.AccessModeRead,
t.Run("ReadOnly", func(t *testing.T) {
const testKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf\n"
ctx, _ := contexttest.MockContext(t, "POST /user2/repo1/settings/keys")
contexttest.MockRequestPostForm(ctx.Req, url.Values{"title": {"read-only"}, "content": {testKey}})
contexttest.LoadRepo(t, ctx, 2)
DeployKeysPost(ctx)
assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{Name: "read-only", Content: testKey, Mode: perm.AccessModeRead})
})
}
func TestAddReadWriteOnlyDeployKey(t *testing.T) {
defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "user2/repo1/settings/keys")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 2)
addKeyForm := forms.AddKeyForm{
Title: "read-write",
Content: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
IsWritable: true,
}
web.SetForm(ctx, &addKeyForm)
DeployKeysPost(ctx)
assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
Name: addKeyForm.Title,
Content: addKeyForm.Content,
Mode: perm.AccessModeWrite,
t.Run("ReadWrite", func(t *testing.T) {
const testKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEHjnNEfE88W1pvBLdV3otv28x760gdmPao3lVD5uAt9\n"
ctx, _ := contexttest.MockContext(t, "POST /user2/repo1/settings/keys")
contexttest.MockRequestPostForm(ctx.Req, url.Values{"title": {"read-write"}, "content": {testKey}, "is_writable": {"on"}})
contexttest.LoadRepo(t, ctx, 2)
DeployKeysPost(ctx)
assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{Name: "read-write", Content: testKey, Mode: perm.AccessModeWrite})
})
}
+1 -1
View File
@@ -1225,7 +1225,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Group("/keys", func() {
m.Combo("").Get(repo_setting.DeployKeys).
Post(web.Bind(forms.AddKeyForm{}), repo_setting.DeployKeysPost)
Post(repo_setting.DeployKeysPost)
m.Post("/delete", repo_setting.DeleteDeployKey)
})
+49 -14
View File
@@ -26,6 +26,8 @@ import (
"gitea.dev/modules/web"
"gitea.dev/modules/web/middleware"
web_types "gitea.dev/modules/web/types"
"gitea.com/go-chi/binding"
)
// Render represents a template render
@@ -81,17 +83,22 @@ func GetWebContext(ctx context.Context) *Context {
return webCtx
}
// ValidateContext is a special context for form validation middleware. It may be different from other contexts.
type ValidateContext struct {
*Base
}
// GetValidateContext gets a context for middleware form validation
func GetValidateContext(req *http.Request) (ctx *ValidateContext) {
func GetValidateContext(req *http.Request) (ctx *middleware.ValidateContext) {
if ctxAPI, ok := req.Context().Value(apiContextKey).(*APIContext); ok {
ctx = &ValidateContext{Base: ctxAPI.Base}
ctx = &middleware.ValidateContext{
Data: ctxAPI.Data,
Locale: ctxAPI.Locale,
Req: ctxAPI.Req,
Resp: ctxAPI.Resp,
}
} else if ctxWeb, ok := req.Context().Value(WebContextKey).(*Context); ok {
ctx = &ValidateContext{Base: ctxWeb.Base}
ctx = &middleware.ValidateContext{
Data: ctxWeb.Data,
Locale: ctxWeb.Locale,
Req: ctxWeb.Req,
Resp: ctxWeb.Resp,
}
} else {
panic("invalid context, expect either APIContext or Context")
}
@@ -254,15 +261,24 @@ func (ctx *Context) JSONOK() {
ctx.JSON(http.StatusOK, map[string]any{"ok": true}) // this is only a dummy response, frontend seldom uses it
}
func (ctx *Context) JSONError(msg any) {
func buildJsonErrorMap(msg any) map[string]any {
switch v := msg.(type) {
case string:
ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "text"})
return map[string]any{"errorMessage": v, "renderFormat": "text"}
case template.HTML:
ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "html"})
default:
panic(fmt.Sprintf("unsupported type: %T", msg))
return map[string]any{"errorMessage": v, "renderFormat": "html"}
}
panic(fmt.Sprintf("unsupported type: %T", msg))
}
func (ctx *Context) JSONError(msg any) {
ctx.JSON(http.StatusBadRequest, buildJsonErrorMap(msg))
}
func (ctx *Context) JSONErrorWithField(msg any, field string) {
m := buildJsonErrorMap(msg)
m["errorFields"] = []string{field}
ctx.JSON(http.StatusBadRequest, m)
}
func (ctx *Context) JSONErrorNotFound(optMsg ...string) {
@@ -270,5 +286,24 @@ func (ctx *Context) JSONErrorNotFound(optMsg ...string) {
if msg == "" {
msg = ctx.Locale.TrString("error.not_found")
}
ctx.JSON(http.StatusNotFound, map[string]any{"errorMessage": msg, "renderFormat": "text"})
ctx.JSON(http.StatusNotFound, buildJsonErrorMap(msg))
}
func GetFetchActionForm[T interface {
*E
middleware.Form
}, E any](ctx *Context) *E {
if web.IsFormSet(ctx) {
panic("don't mix fetch-action form validation with template-based form validation")
}
middleware.SkipTmplFormValidationError(ctx)
form := T(new(E))
errs := binding.Bind(ctx.Req, form)
errorMessage, fieldName, _ := middleware.BuildValidationErrorForUser(form, ctx.Locale, errs)
if errorMessage != "" {
ctx.Resp.Header().Set("Content-Type", "application/json")
ctx.JSONErrorWithField(errorMessage, fieldName)
return nil
}
return form
}
+6
View File
@@ -104,6 +104,12 @@ func MockPrivateContext(t *testing.T, reqPath string) (*context.PrivateContext,
return ctx, resp
}
func MockRequestPostForm(req *http.Request, formData url.Values) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.PostForm = formData
maps.Copy(req.Form, formData)
}
// LoadRepo load a repo into a test context.
func LoadRepo(t *testing.T, ctx gocontext.Context, repoID int64) {
var doer *user_model.User
+5 -5
View File
@@ -41,19 +41,19 @@ type AdminEditBadgeForm struct {
// Validate validates form fields
func (f *AdminCreateBadgeForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// Validate validates form fields
func (f *AdminEditBadgeForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// Validate validates form fields
func (f *AdminCreateUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// AdminEditUserForm form for admin to create user
@@ -82,7 +82,7 @@ type AdminEditUserForm struct {
// Validate validates form fields
func (f *AdminEditUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// AdminDashboardForm form for admin dashboard operations
@@ -94,5 +94,5 @@ type AdminDashboardForm struct {
// Validate validates form fields
func (f *AdminDashboardForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
+1 -1
View File
@@ -100,5 +100,5 @@ type AuthenticationForm struct {
// Validate validates fields
func (f *AuthenticationForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
+3 -3
View File
@@ -31,7 +31,7 @@ type CreateOrgForm struct {
// Validate validates the fields
func (f *CreateOrgForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// UpdateOrgSettingForm form for updating organization settings
@@ -48,7 +48,7 @@ type UpdateOrgSettingForm struct {
// Validate validates the fields
func (f *UpdateOrgSettingForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
type RenameOrgForm struct {
@@ -76,5 +76,5 @@ type CreateTeamForm struct {
// Validate validates the fields
func (f *CreateTeamForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
+1 -1
View File
@@ -26,5 +26,5 @@ type PackageCleanupRuleForm struct {
func (f *PackageCleanupRuleForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
+2 -2
View File
@@ -22,7 +22,7 @@ type NewBranchForm struct {
// Validate validates the fields
func (f *NewBranchForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// RenameBranchForm form for rename a branch
@@ -34,5 +34,5 @@ type RenameBranchForm struct {
// Validate validates the fields
func (f *RenameBranchForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
+32 -36
View File
@@ -50,7 +50,7 @@ type CreateRepoForm struct {
// Validate validates the fields
func (f *CreateRepoForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// MigrateRepoForm form for migrating repository
@@ -86,7 +86,7 @@ type MigrateRepoForm struct {
// Validate validates the fields
func (f *MigrateRepoForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// RepoSettingForm form for changing repository settings
@@ -163,7 +163,7 @@ type RepoSettingForm struct {
// Validate validates the fields
func (f *RepoSettingForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// ProtectBranchForm form for changing protected branch settings
@@ -205,7 +205,7 @@ type ProtectBranchForm struct {
// Validate validates the fields
func (f *ProtectBranchForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// WebhookForm form for changing web hook
@@ -268,7 +268,7 @@ type NewWebhookForm struct {
// Validate validates the fields
func (f *NewWebhookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// NewGogshookForm form for creating gogs hook
@@ -281,7 +281,7 @@ type NewGogshookForm struct {
// Validate validates the fields
func (f *NewGogshookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// NewSlackHookForm form for creating slack hook
@@ -298,13 +298,9 @@ type NewSlackHookForm struct {
func (f *NewSlackHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
if !webhook.IsValidSlackChannel(strings.TrimSpace(f.Channel)) {
errs = append(errs, binding.Error{
FieldNames: []string{"Channel"},
Classification: "",
Message: ctx.Locale.TrString("repo.settings.add_webhook.invalid_channel_name"),
})
errs = middleware.AddValidationError(errs, "Channel", ctx.Locale.TrString("repo.settings.add_webhook.invalid_channel_name"))
}
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// NewDiscordHookForm form for creating discord hook
@@ -318,7 +314,7 @@ type NewDiscordHookForm struct {
// Validate validates the fields
func (f *NewDiscordHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// NewDingtalkHookForm form for creating dingtalk hook
@@ -330,7 +326,7 @@ type NewDingtalkHookForm struct {
// Validate validates the fields
func (f *NewDingtalkHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// NewTelegramHookForm form for creating telegram hook
@@ -344,7 +340,7 @@ type NewTelegramHookForm struct {
// Validate validates the fields
func (f *NewTelegramHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// NewMatrixHookForm form for creating Matrix hook
@@ -358,7 +354,7 @@ type NewMatrixHookForm struct {
// Validate validates the fields
func (f *NewMatrixHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// NewMSTeamsHookForm form for creating MS Teams hook
@@ -370,7 +366,7 @@ type NewMSTeamsHookForm struct {
// Validate validates the fields
func (f *NewMSTeamsHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// NewFeishuHookForm form for creating feishu hook
@@ -382,7 +378,7 @@ type NewFeishuHookForm struct {
// Validate validates the fields
func (f *NewFeishuHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// NewWechatWorkHookForm form for creating wechatwork hook
@@ -394,7 +390,7 @@ type NewWechatWorkHookForm struct {
// Validate validates the fields
func (f *NewWechatWorkHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// NewPackagistHookForm form for creating packagist hook
@@ -408,7 +404,7 @@ type NewPackagistHookForm struct {
// Validate validates the fields
func (f *NewPackagistHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// CreateIssueForm form for creating issue
@@ -426,7 +422,7 @@ type CreateIssueForm struct {
// Validate validates the fields
func (f *CreateIssueForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// CreateCommentForm form for creating comment
@@ -439,7 +435,7 @@ type CreateCommentForm struct {
// Validate validates the fields
func (f *CreateCommentForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// ReactionForm form for adding and removing reaction
@@ -450,7 +446,7 @@ type ReactionForm struct {
// Validate validates the fields
func (f *ReactionForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// IssueLockForm form for locking an issue
@@ -459,9 +455,9 @@ type IssueLockForm struct {
}
// Validate validates the fields
func (i *IssueLockForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
func (f *IssueLockForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, i, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// CreateProjectForm form for creating a project
@@ -489,7 +485,7 @@ type CreateMilestoneForm struct {
// Validate validates the fields
func (f *CreateMilestoneForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// CreateLabelForm form for creating label
@@ -506,7 +502,7 @@ type CreateLabelForm struct {
// Validate validates the fields
func (f *CreateLabelForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// InitializeLabelsForm form for initializing labels
@@ -517,7 +513,7 @@ type InitializeLabelsForm struct {
// Validate validates the fields
func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// MergePullRequestForm form for merging Pull Request
@@ -571,7 +567,7 @@ func (f *MergePullRequestForm) UnmarshalJSON(b []byte) error {
// Validate validates the fields
func (f *MergePullRequestForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// CodeCommentForm form for adding code comments for PRs
@@ -590,7 +586,7 @@ type CodeCommentForm struct {
// Validate validates the fields
func (f *CodeCommentForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// SubmitReviewForm for submitting a finished code review
@@ -604,7 +600,7 @@ type SubmitReviewForm struct {
// Validate validates the fields
func (f *SubmitReviewForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// ReviewType will return the corresponding ReviewType for type
@@ -665,7 +661,7 @@ type NewReleaseForm struct {
// Validate validates the fields
func (f *NewReleaseForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// GenerateReleaseNotesForm retrieves release notes recommendations.
@@ -678,7 +674,7 @@ type GenerateReleaseNotesForm struct {
// Validate validates the fields
func (f *GenerateReleaseNotesForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// EditReleaseForm form for changing release
@@ -693,7 +689,7 @@ type EditReleaseForm struct {
// Validate validates the fields
func (f *EditReleaseForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// __ __.__ __ .__
@@ -714,7 +710,7 @@ type NewWikiForm struct {
// FIXME: use code generation to generate this method.
func (f *NewWikiForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// ___________.__ ___________ __
@@ -733,7 +729,7 @@ type AddTimeManuallyForm struct {
// Validate validates the fields
func (f *AddTimeManuallyForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// SaveTopicForm form for save topics for repository
+1 -1
View File
@@ -26,7 +26,7 @@ type CommitCommonForm struct {
func (f *CommitCommonForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
type CommitCommonFormInterface interface {
+1 -1
View File
@@ -22,5 +22,5 @@ type ProtectTagForm struct {
// Validate validates the fields
func (f *ProtectTagForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
+1 -1
View File
@@ -20,5 +20,5 @@ type EditRunnerForm struct {
// Validate validates form fields
func (f *EditRunnerForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
+26 -26
View File
@@ -77,7 +77,7 @@ type InstallForm struct {
// Validate validates the fields
func (f *InstallForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// _____ ____ _________________ ___
@@ -98,7 +98,7 @@ type RegisterForm struct {
// Validate validates the fields
func (f *RegisterForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// IsEmailDomainAllowed validates that the email address
@@ -120,7 +120,7 @@ type MustChangePasswordForm struct {
// Validate validates the fields
func (f *MustChangePasswordForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// SignInForm form for signing in with user/password
@@ -134,7 +134,7 @@ type SignInForm struct {
// Validate validates the fields
func (f *SignInForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// AuthorizationForm form for authorizing oauth2 clients
@@ -154,7 +154,7 @@ type AuthorizationForm struct {
// Validate validates the fields
func (f *AuthorizationForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// GrantApplicationForm form for authorizing oauth2 clients
@@ -170,7 +170,7 @@ type GrantApplicationForm struct {
// Validate validates the fields
func (f *GrantApplicationForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// AccessTokenForm for issuing access tokens from authorization codes or refresh tokens
@@ -189,7 +189,7 @@ type AccessTokenForm struct {
// Validate validates the fields
func (f *AccessTokenForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// IntrospectTokenForm for introspecting tokens
@@ -200,7 +200,7 @@ type IntrospectTokenForm struct {
// Validate validates the fields
func (f *IntrospectTokenForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// __________________________________________.___ _______ ________ _________
@@ -225,7 +225,7 @@ type UpdateProfileForm struct {
// Validate validates the fields
func (f *UpdateProfileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// UpdateLanguageForm form for updating profile
@@ -236,7 +236,7 @@ type UpdateLanguageForm struct {
// Validate validates the fields
func (f *UpdateLanguageForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// Avatar types
@@ -256,7 +256,7 @@ type AvatarForm struct {
// Validate validates the fields
func (f *AvatarForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// AddEmailForm form for adding new email
@@ -267,7 +267,7 @@ type AddEmailForm struct {
// Validate validates the fields
func (f *AddEmailForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// UpdateThemeForm form for updating a users' theme
@@ -278,7 +278,7 @@ type UpdateThemeForm struct {
// Validate validates the field
func (f *UpdateThemeForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// ChangePasswordForm form for changing password
@@ -291,7 +291,7 @@ type ChangePasswordForm struct {
// Validate validates the fields
func (f *ChangePasswordForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// AddOpenIDForm is for changing openid uri
@@ -302,7 +302,7 @@ type AddOpenIDForm struct {
// Validate validates the fields
func (f *AddOpenIDForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// AddKeyForm form for adding SSH/GPG key
@@ -319,7 +319,7 @@ type AddKeyForm struct {
// Validate validates the fields
func (f *AddKeyForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// AddSecretForm for adding secrets
@@ -332,7 +332,7 @@ type AddSecretForm struct {
// Validate validates the fields
func (f *AddSecretForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
type EditVariableForm struct {
@@ -343,7 +343,7 @@ type EditVariableForm struct {
func (f *EditVariableForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// NewAccessTokenForm form for creating access token
@@ -354,7 +354,7 @@ type NewAccessTokenForm struct {
// Validate validates the fields
func (f *NewAccessTokenForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// EditOAuth2ApplicationForm form for editing oauth2 applications
@@ -381,9 +381,9 @@ func (f *EditOAuth2ApplicationForm) Validate(req *http.Request, errs binding.Err
ctx := context.GetValidateContext(req)
invalidURI := DetectInvalidOAuth2ApplicationRedirectURI(util.SplitTrimSpace(f.RedirectURIs, "\n"))
if invalidURI != "" {
errs = middleware.ReportValidationError(errs, ctx.Data, "RedirectURIs", binding.ERR_URL, ctx.Locale.TrString("form.url_error", invalidURI))
errs = middleware.AddValidationError(errs, "RedirectURIs", "RedirectURIs: "+ctx.Locale.TrString("form.url_error", `"`+invalidURI+`"`))
}
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// TwoFactorAuthForm for logging in with 2FA token.
@@ -394,7 +394,7 @@ type TwoFactorAuthForm struct {
// Validate validates the fields
func (f *TwoFactorAuthForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// TwoFactorScratchAuthForm for logging in with 2FA scratch token.
@@ -405,7 +405,7 @@ type TwoFactorScratchAuthForm struct {
// Validate validates the fields
func (f *TwoFactorScratchAuthForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// WebauthnRegistrationForm for reserving an WebAuthn name
@@ -416,7 +416,7 @@ type WebauthnRegistrationForm struct {
// Validate validates the fields
func (f *WebauthnRegistrationForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// PackageSettingForm form for package settings
@@ -428,7 +428,7 @@ type PackageSettingForm struct {
// Validate validates the fields
func (f *PackageSettingForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
type BlockUserForm struct {
@@ -439,5 +439,5 @@ type BlockUserForm struct {
func (f *BlockUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
+3 -3
View File
@@ -21,7 +21,7 @@ type SignInOpenIDForm struct {
// Validate validates the fields
func (f *SignInOpenIDForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// SignUpOpenIDForm form for signin up with OpenID
@@ -33,7 +33,7 @@ type SignUpOpenIDForm struct {
// Validate validates the fields
func (f *SignUpOpenIDForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
// ConnectOpenIDForm form for connecting an existing account to an OpenID URI
@@ -45,5 +45,5 @@ type ConnectOpenIDForm struct {
// Validate validates the fields
func (f *ConnectOpenIDForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
return middleware.Validate(ctx, errs, f)
}
+5 -5
View File
@@ -11,21 +11,21 @@
</div>
</h4>
<div class="ui attached segment">
<div class="{{if not .HasError}}tw-hidden{{end}} tw-mb-4" id="add-deploy-key-panel">
<form class="ui form" action="{{.Link}}" method="post">
<div class="tw-hidden tw-mb-4" id="add-deploy-key-panel">
<form class="ui form form-fetch-action" action="{{.Link}}" method="post">
<div class="field">
{{ctx.Locale.Tr "repo.settings.deploy_key_desc"}}
</div>
<div class="field {{if .Err_Title}}error{{end}}">
<div class="field">
<label for="ssh-key-title">{{ctx.Locale.Tr "repo.settings.title"}}</label>
<input id="ssh-key-title" name="title" value="{{.title}}" autofocus required>
</div>
<div class="field {{if .Err_Content}}error{{end}}">
<div class="field">
<label for="ssh-key-content">{{ctx.Locale.Tr "repo.settings.deploy_key_content"}}</label>
<textarea id="ssh-key-content" name="content" placeholder="{{ctx.Locale.Tr "settings.key_content_ssh_placeholder"}}" required>{{.content}}</textarea>
</div>
<div class="field">
<div class="ui checkbox {{if .Err_IsWritable}}error{{end}}">
<div class="ui checkbox">
<input id="ssh-key-is-writable" name="is_writable" type="checkbox" value="1">
<label for="ssh-key-is-writable">
{{ctx.Locale.Tr "repo.settings.is_writable"}}
+2 -2
View File
@@ -53,7 +53,7 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) {
OldRefSubURL: "branch/master",
NewBranch: "",
ExpectedStatus: http.StatusSeeOther,
FlashMessage: translation.NewLocale("en-US").TrString("form.NewBranchName") + translation.NewLocale("en-US").TrString("form.require_error"),
FlashMessage: translation.NewLocale("en-US").TrString("form.require_error", translation.NewLocale("en-US").TrString("form.NewBranchName")),
},
{
OldRefSubURL: "branch/master",
@@ -65,7 +65,7 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) {
OldRefSubURL: "branch/master",
NewBranch: strings.Repeat("b", 101),
ExpectedStatus: http.StatusSeeOther,
FlashMessage: translation.NewLocale("en-US").TrString("form.NewBranchName") + translation.NewLocale("en-US").TrString("form.max_size_error", "100"),
FlashMessage: translation.NewLocale("en-US").TrString("form.max_size_error", translation.NewLocale("en-US").TrString("form.NewBranchName"), "100"),
},
{
OldRefSubURL: "branch/master",
+1 -1
View File
@@ -184,7 +184,7 @@ func testRenameInvalidUsername(t *testing.T) {
htmlDoc := NewHTMLParser(t, resp.Body)
assert.Contains(t,
htmlDoc.doc.Find(".ui.negative.message").Text(),
translation.NewLocale("en-US").TrString("form.username_error"),
translation.NewLocale("en-US").TrString("form.username_error", "Name"),
)
unittest.AssertNotExistsBean(t, &user_model.User{Name: invalidUsername})
@@ -1,4 +1,6 @@
import {execPseudoSelectorCommands, handleFetchActionSuccessJson} from './common-fetch-action.ts';
import {execPseudoSelectorCommands, handleFetchActionErrorFields, handleFetchActionSuccessJson} from './common-fetch-action.ts';
import {createElementFromHTML} from '../utils/dom.ts';
import {normalizeTestHtml} from '../utils/testhelper.ts';
test('execPseudoSelectorCommands', () => {
window.document.body.innerHTML = `
@@ -57,3 +59,15 @@ test('handleFetchActionSuccessJson', async () => {
expect(spyReload).toHaveBeenCalledTimes(1);
vi.resetAllMocks();
});
test('handleFetchActionErrorFields', () => {
const elForm = createElementFromHTML<HTMLElement>(`<form>
<div class="error field"></div>
<div class="field"><input name="Foo_Bar[]"></div>
</form>`);
handleFetchActionErrorFields(elForm, ['foo-BAR', 'other']);
expect(normalizeTestHtml(elForm.outerHTML)).toEqual(normalizeTestHtml(`<form>
<div class="field"></div>
<div class="field error"><input name="Foo_Bar[]"></div>
</form>`));
});
+36 -5
View File
@@ -1,6 +1,6 @@
import {GET, request} from '../modules/fetch.ts';
import {hideToastsAll, showErrorToast} from '../modules/toast.ts';
import {activePageTimerRefresh, addDelegatedEventListener, createElementFromHTML} from '../utils/dom.ts';
import {activePageTimerRefresh, addDelegatedEventListener, createElementFromHTML, queryElems} from '../utils/dom.ts';
import {errorMessage, errorName} from '../modules/errors.ts';
import {confirmModal, createConfirmModal} from './comp/ConfirmModal.ts';
import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
@@ -92,7 +92,34 @@ async function handleFetchActionSuccess(el: HTMLElement, opt: FetchActionOpts, r
}
}
async function handleFetchActionError(resp: Response) {
function resetFormErrorFields(elForm: HTMLFormElement) {
queryElems(elForm, '.field.error', (el) => el.classList.remove('error'));
}
export function handleFetchActionErrorFields(el: HTMLElement, errorFields: string[]) {
// The "error field" only works in a form.
// And non-form requests do not need such "error field" because the requests are just from a button or a link in such cases.
if (el.nodeName !== 'FORM') return;
const elForm = el as HTMLFormElement;
resetFormErrorFields(elForm);
const fieldNameElemMap : Record<string, HTMLElement> = {};
const normalizeFieldName = (name: string) => name.replace(/[^A-Za-z0-9]/g, '').toLowerCase();
queryElems(elForm, '[name]', (el) => {
const name = el.getAttribute('name');
if (!name) return;
fieldNameElemMap[normalizeFieldName(name)] = el;
});
for (const errorField of errorFields) {
const name = normalizeFieldName(errorField);
const elInput = fieldNameElemMap[name];
if (!elInput) continue;
const elField = elInput.closest('.field');
if (!elField) continue;
elField.classList.add('error');
}
}
async function handleFetchActionError(el: HTMLElement, resp: Response) {
const isRespJson = resp.headers.get('content-type')?.includes('application/json');
const respText = await resp.text();
const respJson = isRespJson ? JSON.parse(respText) : null;
@@ -100,6 +127,9 @@ async function handleFetchActionError(resp: Response) {
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
showErrorToast(respJson.errorMessage, {useHtmlBody: respJson.renderFormat === 'html'});
if (respJson?.errorFields?.length) {
handleFetchActionErrorFields(el, respJson?.errorFields);
}
} else {
showErrorToast(`Error ${resp.status} ${resp.statusText}. Response: ${respText.substring(0, 200)}`);
}
@@ -135,7 +165,7 @@ export async function performFetchActionRequest(el: HTMLElement, opt: FetchActio
headers.set('X-Gitea-Fetch-Action', '1');
const resp = await request(url, {method: opt.method, data: opt.data, headers});
if (resp.ok) return resp;
await handleFetchActionError(resp);
await handleFetchActionError(el, resp);
} catch (err) {
if (errorName(err) !== 'AbortError') {
console.error(`Fetch action request error:`, err);
@@ -194,9 +224,10 @@ function prepareFormFetchActionOpts(formEl: HTMLFormElement, opts: SubmitFormFet
};
}
export async function submitFormFetchAction(formEl: HTMLFormElement, opts: SubmitFormFetchActionOpts = {}) {
export async function submitFormFetchAction(elForm: HTMLFormElement, opts: SubmitFormFetchActionOpts = {}) {
hideToastsAll();
await performFetchAction(formEl, prepareFormFetchActionOpts(formEl, opts));
resetFormErrorFields(elForm);
await performFetchAction(elForm, prepareFormFetchActionOpts(elForm, opts));
}
async function confirmFetchAction(el: HTMLElement) {