// Copyright 2022 E99p1ant. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package form import ( "mime" "mime/multipart" "net/http" "reflect" "github.com/flamego/flamego" "github.com/flamego/template" "github.com/unknwon/com" "github.com/wuhan005/govalid" "github.com/tamakyi/TamaBox/internal/context" ) type ErrorCategory string const ( ErrorCategoryDeserialization ErrorCategory = "deserialization" ErrorCategoryValidation ErrorCategory = "validation" ) type Error struct { Category ErrorCategory Error error } const ( multipartMemoryLimit = 10 << 20 // 10 MiB:multipart 内存驻留上限,超出由 mime/multipart 落盘 // MaxRequestBodySize 请求体总大小上限(含落盘部分),略高于内存上限以留出开销。 // // ⚠️ 必须显式限制:ParseMultipartForm 的参数只约束内存缓冲,超出部分会无上限写入 // 临时文件;匿名可达的提问接口即接受 multipart,无总量限制则可持续撑满磁盘。 // 导出供 internal/route/bodylimit.go 的全局中间件复用——仅靠本 Bind 内限制不够, // 组级 CSRF 校验先于 Bind 执行且会整包惰性解析 body,须把上限提前到一切解析之前。 MaxRequestBodySize = 12 << 20 // 12 MiB ) func Bind(model interface{}) flamego.Handler { if reflect.TypeOf(model).Kind() == reflect.Ptr { panic("form: pointer can not be accepted as binding model") } return func(c context.Context, data template.Data) { obj := reflect.New(reflect.TypeOf(model)) defer func() { c.Map(obj.Elem().Interface()) }() r := c.Request() // 在任何解析之前封顶请求体总大小。c.Request() 是 flamego 包装类型(Body 是方法 // 而非字段),须取内嵌 *http.Request 才能替换。超限后 Read 报错,使 ParseForm / // ParseMultipartForm 失败并走反序列化错误分支,避免超大 body 落盘或读入内存。 httpReq := r.Request httpReq.Body = http.MaxBytesReader(c.ResponseWriter(), httpReq.Body, MaxRequestBodySize) contentType, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) switch contentType { case "application/x-www-form-urlencoded", "": if err := r.ParseForm(); err != nil { // ⚠️ 必须 SetError:flamego 的 handler 链只要响应未写入就会自动执行下一 handler, // 仅 Map(Error) 不中断链,零值结构体会照常进入业务动作(静默清空屏蔽词、解除 // 页面密码等)。SetError 置位 HasError,action 开头的 abortOnFormError 守卫会早退。 c.SetError(err) c.Map(Error{Category: ErrorCategoryDeserialization, Error: err}) return } case "multipart/form-data": if err := r.ParseMultipartForm(multipartMemoryLimit); err != nil { c.SetError(err) c.Map(Error{Category: ErrorCategoryDeserialization, Error: err}) return } } typ := reflect.TypeOf(obj.Interface()) val := reflect.ValueOf(obj.Interface()) if typ.Kind() == reflect.Ptr { typ = typ.Elem() val = val.Elem() } for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) fieldName := typ.Field(i).Tag.Get("form") if fieldName == "" { fieldName = com.ToSnakeCase(field.Name) } if r.MultipartForm != nil { fhType := reflect.TypeOf((*multipart.FileHeader)(nil)) value, ok := r.MultipartForm.Value[fieldName] if ok { if val.Field(i).Kind() == reflect.Slice && val.Field(i).Type().Elem() == fhType { continue } // FIXME: We don't implement the slice type yet. val.Field(i).Set(reflect.ValueOf(value[0])) } else { value, ok := r.MultipartForm.File[fieldName] if ok { numElems := len(value) if val.Field(i).Kind() == reflect.Slice && numElems > 0 && val.Field(i).Type().Elem() == fhType { slice := reflect.MakeSlice(val.Field(i).Type(), numElems, numElems) for i := 0; i < numElems; i++ { slice.Index(i).Set(reflect.ValueOf(value[i])) } val.Field(i).Set(slice) } else if val.Field(i).Type() == fhType { val.Field(i).Set(reflect.ValueOf(value[0])) } } } } else { // 非 multipart 请求仅绑定 string 字段,其余类型(如 []*multipart.FileHeader // 图片字段)保持零值,否则 reflect.Set 会因类型不匹配 panic。 // 带 enctype 的提问表单走上面分支从未触发,纯文本提交(如私聊)会命中此路径。 if val.Field(i).Kind() == reflect.String { val.Field(i).Set(reflect.ValueOf(r.Form.Get(fieldName))) } } } errors, ok := govalid.Check(obj.Interface()) if !ok { Assign(obj.Interface(), data) c.SetError(errors[0]) c.Map(Error{Category: ErrorCategoryValidation, Error: errors[0]}) return } } } // Assign assigns form values back to the template data. func Assign(form interface{}, data map[string]interface{}) { typ := reflect.TypeOf(form) val := reflect.ValueOf(form) if typ.Kind() == reflect.Ptr { typ = typ.Elem() val = val.Elem() } for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) fieldName := com.ToSnakeCase(field.Name) data[fieldName] = val.Field(i).Interface() } }