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
+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
}